@tdk-landscape/tdk-cli-core 1.3.49 → 1.3.51

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 (311) hide show
  1. package/.claude/skills/layer-autoresearch/SKILL.md +48 -0
  2. package/.claude/skills/tdk-troubleshoot/SKILL.md +222 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
  6. package/.github/dependabot.yml +33 -0
  7. package/.github/pull_request_template.md +5 -0
  8. package/.github/workflows/ci.yml +187 -0
  9. package/.github/workflows/release-binaries.yml +19 -9
  10. package/.github/zizmor.yml +9 -0
  11. package/CODE_OF_CONDUCT.md +9 -0
  12. package/CONTRIBUTING.md +42 -0
  13. package/LICENSE +21 -0
  14. package/README.md +151 -0
  15. package/SECURITY.md +17 -0
  16. package/benchmarks/results/container-scale-2026-09-25T20-54-49-736Z.json +45 -0
  17. package/benchmarks/results/container-scale-2026-09-25T21-01-20-540Z.json +150 -0
  18. package/benchmarks/results/container-scale-2026-09-25T21-15-01-441Z.json +73 -0
  19. package/benchmarks/results/container-scale-2026-09-25T22-09-01-630Z.json +77 -0
  20. package/benchmarks/results/container-scale-2026-09-25T22-18-28-371Z.json +202 -0
  21. package/cli/README.md +25 -0
  22. package/cli/biome.json +1 -1
  23. package/cli/dist/cli.d.ts +3 -0
  24. package/cli/dist/cli.d.ts.map +1 -0
  25. package/cli/dist/cli.js +64 -0
  26. package/cli/dist/cli.js.map +1 -0
  27. package/cli/dist/commands/completion.d.ts +3 -0
  28. package/cli/dist/commands/completion.d.ts.map +1 -0
  29. package/cli/dist/commands/completion.js +294 -0
  30. package/cli/dist/commands/completion.js.map +1 -0
  31. package/cli/dist/commands/config.d.ts +3 -0
  32. package/cli/dist/commands/config.d.ts.map +1 -0
  33. package/cli/dist/commands/config.js +231 -0
  34. package/cli/dist/commands/config.js.map +1 -0
  35. package/cli/dist/commands/doctor.d.ts +19 -0
  36. package/cli/dist/commands/doctor.d.ts.map +1 -0
  37. package/cli/dist/commands/doctor.js +745 -0
  38. package/cli/dist/commands/doctor.js.map +1 -0
  39. package/cli/dist/commands/down.d.ts +3 -0
  40. package/cli/dist/commands/down.d.ts.map +1 -0
  41. package/cli/dist/commands/down.js +32 -0
  42. package/cli/dist/commands/down.js.map +1 -0
  43. package/cli/dist/commands/help.d.ts +2 -0
  44. package/cli/dist/commands/help.d.ts.map +1 -0
  45. package/cli/dist/commands/help.js +138 -0
  46. package/cli/dist/commands/help.js.map +1 -0
  47. package/cli/dist/commands/networks.d.ts +3 -0
  48. package/cli/dist/commands/networks.d.ts.map +1 -0
  49. package/cli/dist/commands/networks.js +260 -0
  50. package/cli/dist/commands/networks.js.map +1 -0
  51. package/cli/dist/commands/project.d.ts +3 -0
  52. package/cli/dist/commands/project.d.ts.map +1 -0
  53. package/cli/dist/commands/project.js +370 -0
  54. package/cli/dist/commands/project.js.map +1 -0
  55. package/cli/dist/commands/projects.d.ts +3 -0
  56. package/cli/dist/commands/projects.d.ts.map +1 -0
  57. package/cli/dist/commands/projects.js +71 -0
  58. package/cli/dist/commands/projects.js.map +1 -0
  59. package/cli/dist/commands/resource.d.ts +69 -0
  60. package/cli/dist/commands/resource.d.ts.map +1 -0
  61. package/cli/dist/commands/resource.js +579 -0
  62. package/cli/dist/commands/resource.js.map +1 -0
  63. package/cli/dist/commands/resources.d.ts +3 -0
  64. package/cli/dist/commands/resources.d.ts.map +1 -0
  65. package/cli/dist/commands/resources.js +73 -0
  66. package/cli/dist/commands/resources.js.map +1 -0
  67. package/cli/dist/commands/stack.d.ts +3 -0
  68. package/cli/dist/commands/stack.d.ts.map +1 -0
  69. package/cli/dist/commands/stack.js +97 -0
  70. package/cli/dist/commands/stack.js.map +1 -0
  71. package/cli/dist/commands/stacks.d.ts +3 -0
  72. package/cli/dist/commands/stacks.d.ts.map +1 -0
  73. package/cli/dist/commands/stacks.js +43 -0
  74. package/cli/dist/commands/stacks.js.map +1 -0
  75. package/cli/dist/commands/status.d.ts +3 -0
  76. package/cli/dist/commands/status.d.ts.map +1 -0
  77. package/cli/dist/commands/status.js +71 -0
  78. package/cli/dist/commands/status.js.map +1 -0
  79. package/cli/dist/commands/ui.d.ts +3 -0
  80. package/cli/dist/commands/ui.d.ts.map +1 -0
  81. package/cli/dist/commands/ui.js +445 -0
  82. package/cli/dist/commands/ui.js.map +1 -0
  83. package/cli/dist/commands/up.d.ts +3 -0
  84. package/cli/dist/commands/up.d.ts.map +1 -0
  85. package/cli/dist/commands/up.js +124 -0
  86. package/cli/dist/commands/up.js.map +1 -0
  87. package/cli/dist/commands/upgrade.d.ts +11 -0
  88. package/cli/dist/commands/upgrade.d.ts.map +1 -0
  89. package/cli/dist/commands/upgrade.js +496 -0
  90. package/cli/dist/commands/upgrade.js.map +1 -0
  91. package/cli/dist/commands/version.d.ts +3 -0
  92. package/cli/dist/commands/version.d.ts.map +1 -0
  93. package/cli/dist/commands/version.js +9 -0
  94. package/cli/dist/commands/version.js.map +1 -0
  95. package/cli/dist/components/Accessible.d.ts +8 -0
  96. package/cli/dist/components/Accessible.d.ts.map +1 -0
  97. package/cli/dist/components/Accessible.js +10 -0
  98. package/cli/dist/components/Accessible.js.map +1 -0
  99. package/cli/dist/components/BaseTooltip.d.ts +4 -0
  100. package/cli/dist/components/BaseTooltip.d.ts.map +1 -0
  101. package/cli/dist/components/BaseTooltip.js +28 -0
  102. package/cli/dist/components/BaseTooltip.js.map +1 -0
  103. package/cli/dist/components/DetailPanel.d.ts +4 -0
  104. package/cli/dist/components/DetailPanel.d.ts.map +1 -0
  105. package/cli/dist/components/DetailPanel.js +18 -0
  106. package/cli/dist/components/DetailPanel.js.map +1 -0
  107. package/cli/dist/components/FileTree.d.ts +4 -0
  108. package/cli/dist/components/FileTree.d.ts.map +1 -0
  109. package/cli/dist/components/FileTree.js +47 -0
  110. package/cli/dist/components/FileTree.js.map +1 -0
  111. package/cli/dist/components/ResourceSelectInput.d.ts +4 -0
  112. package/cli/dist/components/ResourceSelectInput.d.ts.map +1 -0
  113. package/cli/dist/components/ResourceSelectInput.js +7 -0
  114. package/cli/dist/components/ResourceSelectInput.js.map +1 -0
  115. package/cli/dist/components/ResourceTable.d.ts +4 -0
  116. package/cli/dist/components/ResourceTable.d.ts.map +1 -0
  117. package/cli/dist/components/ResourceTable.js +18 -0
  118. package/cli/dist/components/ResourceTable.js.map +1 -0
  119. package/cli/dist/components/TabBar.d.ts +4 -0
  120. package/cli/dist/components/TabBar.d.ts.map +1 -0
  121. package/cli/dist/components/TabBar.js +17 -0
  122. package/cli/dist/components/TabBar.js.map +1 -0
  123. package/cli/dist/components/Tooltip.d.ts +12 -0
  124. package/cli/dist/components/Tooltip.d.ts.map +1 -0
  125. package/cli/dist/components/Tooltip.js +12 -0
  126. package/cli/dist/components/Tooltip.js.map +1 -0
  127. package/cli/dist/components/index.d.ts +8 -0
  128. package/cli/dist/components/index.d.ts.map +1 -0
  129. package/cli/dist/components/index.js +8 -0
  130. package/cli/dist/components/index.js.map +1 -0
  131. package/cli/dist/config/platform-standards.d.ts +159 -0
  132. package/cli/dist/config/platform-standards.d.ts.map +1 -0
  133. package/cli/dist/config/platform-standards.js +181 -0
  134. package/cli/dist/config/platform-standards.js.map +1 -0
  135. package/cli/dist/generator/extension-fetch.d.ts +25 -0
  136. package/cli/dist/generator/extension-fetch.d.ts.map +1 -0
  137. package/cli/dist/generator/extension-fetch.js +259 -0
  138. package/cli/dist/generator/extension-fetch.js.map +1 -0
  139. package/cli/dist/generator/template-engine.d.ts +62 -0
  140. package/cli/dist/generator/template-engine.d.ts.map +1 -0
  141. package/cli/dist/generator/template-engine.js +623 -0
  142. package/cli/dist/generator/template-engine.js.map +1 -0
  143. package/cli/dist/index.d.ts +11 -0
  144. package/cli/dist/index.d.ts.map +1 -0
  145. package/cli/dist/index.js +11 -0
  146. package/cli/dist/index.js.map +1 -0
  147. package/cli/dist/types/index.d.ts +257 -0
  148. package/cli/dist/types/index.d.ts.map +1 -0
  149. package/cli/dist/types/index.js +17 -0
  150. package/cli/dist/types/index.js.map +1 -0
  151. package/cli/dist/utils/cache.d.ts +19 -0
  152. package/cli/dist/utils/cache.d.ts.map +1 -0
  153. package/cli/dist/utils/cache.js +64 -0
  154. package/cli/dist/utils/cache.js.map +1 -0
  155. package/cli/dist/utils/command-helpers.d.ts +17 -0
  156. package/cli/dist/utils/command-helpers.d.ts.map +1 -0
  157. package/cli/dist/utils/command-helpers.js +41 -0
  158. package/cli/dist/utils/command-helpers.js.map +1 -0
  159. package/cli/dist/utils/constants.d.ts +53 -0
  160. package/cli/dist/utils/constants.d.ts.map +1 -0
  161. package/cli/dist/utils/constants.js +115 -0
  162. package/cli/dist/utils/constants.js.map +1 -0
  163. package/cli/dist/utils/discovery-context.d.ts +4 -0
  164. package/cli/dist/utils/discovery-context.d.ts.map +1 -0
  165. package/cli/dist/utils/discovery-context.js +39 -0
  166. package/cli/dist/utils/discovery-context.js.map +1 -0
  167. package/cli/dist/utils/docker.d.ts +4 -0
  168. package/cli/dist/utils/docker.d.ts.map +1 -0
  169. package/cli/dist/utils/docker.js +19 -0
  170. package/cli/dist/utils/docker.js.map +1 -0
  171. package/cli/dist/utils/doctor-runtime.d.ts +81 -0
  172. package/cli/dist/utils/doctor-runtime.d.ts.map +1 -0
  173. package/cli/dist/utils/doctor-runtime.js +498 -0
  174. package/cli/dist/utils/doctor-runtime.js.map +1 -0
  175. package/cli/dist/utils/env-validator.d.ts +15 -0
  176. package/cli/dist/utils/env-validator.d.ts.map +1 -0
  177. package/cli/dist/utils/env-validator.js +109 -0
  178. package/cli/dist/utils/env-validator.js.map +1 -0
  179. package/cli/dist/utils/errors.d.ts +31 -0
  180. package/cli/dist/utils/errors.d.ts.map +1 -0
  181. package/cli/dist/utils/errors.js +140 -0
  182. package/cli/dist/utils/errors.js.map +1 -0
  183. package/cli/dist/utils/file-helpers.d.ts +14 -0
  184. package/cli/dist/utils/file-helpers.d.ts.map +1 -0
  185. package/cli/dist/utils/file-helpers.js +45 -0
  186. package/cli/dist/utils/file-helpers.js.map +1 -0
  187. package/cli/dist/utils/formatting.d.ts +30 -0
  188. package/cli/dist/utils/formatting.d.ts.map +1 -0
  189. package/cli/dist/utils/formatting.js +164 -0
  190. package/cli/dist/utils/formatting.js.map +1 -0
  191. package/cli/dist/utils/paths.d.ts +3 -0
  192. package/cli/dist/utils/paths.d.ts.map +1 -0
  193. package/cli/dist/utils/paths.js +41 -0
  194. package/cli/dist/utils/paths.js.map +1 -0
  195. package/cli/dist/utils/port-assignment.d.ts +6 -0
  196. package/cli/dist/utils/port-assignment.d.ts.map +1 -0
  197. package/cli/dist/utils/port-assignment.js +81 -0
  198. package/cli/dist/utils/port-assignment.js.map +1 -0
  199. package/cli/dist/utils/project-features.d.ts +23 -0
  200. package/cli/dist/utils/project-features.d.ts.map +1 -0
  201. package/cli/dist/utils/project-features.js +98 -0
  202. package/cli/dist/utils/project-features.js.map +1 -0
  203. package/cli/dist/utils/project-templates.d.ts +7 -0
  204. package/cli/dist/utils/project-templates.d.ts.map +1 -0
  205. package/cli/dist/utils/project-templates.js +24 -0
  206. package/cli/dist/utils/project-templates.js.map +1 -0
  207. package/cli/dist/utils/prompt.d.ts +28 -0
  208. package/cli/dist/utils/prompt.d.ts.map +1 -0
  209. package/cli/dist/utils/prompt.js +79 -0
  210. package/cli/dist/utils/prompt.js.map +1 -0
  211. package/cli/dist/utils/resource-features.d.ts +29 -0
  212. package/cli/dist/utils/resource-features.d.ts.map +1 -0
  213. package/cli/dist/utils/resource-features.js +175 -0
  214. package/cli/dist/utils/resource-features.js.map +1 -0
  215. package/cli/dist/utils/service-urls.d.ts +30 -0
  216. package/cli/dist/utils/service-urls.d.ts.map +1 -0
  217. package/cli/dist/utils/service-urls.js +93 -0
  218. package/cli/dist/utils/service-urls.js.map +1 -0
  219. package/cli/dist/utils/services.d.ts +18 -0
  220. package/cli/dist/utils/services.d.ts.map +1 -0
  221. package/cli/dist/utils/services.js +292 -0
  222. package/cli/dist/utils/services.js.map +1 -0
  223. package/cli/dist/utils/stack-features.d.ts +22 -0
  224. package/cli/dist/utils/stack-features.d.ts.map +1 -0
  225. package/cli/dist/utils/stack-features.js +13 -0
  226. package/cli/dist/utils/stack-features.js.map +1 -0
  227. package/cli/dist/utils/tar.d.ts +7 -0
  228. package/cli/dist/utils/tar.d.ts.map +1 -0
  229. package/cli/dist/utils/tar.js +21 -0
  230. package/cli/dist/utils/tar.js.map +1 -0
  231. package/cli/dist/utils/tilt.d.ts +20 -0
  232. package/cli/dist/utils/tilt.d.ts.map +1 -0
  233. package/cli/dist/utils/tilt.js +95 -0
  234. package/cli/dist/utils/tilt.js.map +1 -0
  235. package/cli/dist/utils/validation.d.ts +16 -0
  236. package/cli/dist/utils/validation.d.ts.map +1 -0
  237. package/cli/dist/utils/validation.js +58 -0
  238. package/cli/dist/utils/validation.js.map +1 -0
  239. package/cli/package.json +11 -12
  240. package/cli/src/commands/__tests__/always-enabled-infra.test.ts +2 -8
  241. package/cli/src/commands/__tests__/doctor.test.ts +181 -8
  242. package/cli/src/commands/__tests__/phases-model.test.ts +2 -4
  243. package/cli/src/commands/__tests__/project-resource-features-e2e.test.ts +4 -24
  244. package/cli/src/commands/__tests__/saas-starter-integration.test.ts +8 -10
  245. package/cli/src/commands/__tests__/stack-features-e2e.test.ts +10 -4
  246. package/cli/src/commands/__tests__/upgrade.test.ts +23 -13
  247. package/cli/src/commands/doctor.ts +136 -0
  248. package/cli/src/commands/help.ts +13 -3
  249. package/cli/src/commands/project.ts +11 -14
  250. package/cli/src/commands/projects.ts +2 -2
  251. package/cli/src/commands/up.ts +13 -4
  252. package/cli/src/commands/upgrade.ts +11 -4
  253. package/cli/src/config/platform-standards.test.ts +6 -6
  254. package/cli/src/generator/__tests__/extension-fetch.test.ts +36 -14
  255. package/cli/src/generator/extension-fetch.ts +12 -5
  256. package/cli/src/generator/template-engine.ts +19 -21
  257. package/cli/src/types/index.ts +2 -0
  258. package/cli/src/utils/__tests__/tilt.test.ts +29 -0
  259. package/cli/src/utils/constants.ts +4 -0
  260. package/cli/src/utils/doctor-runtime.ts +67 -27
  261. package/cli/src/utils/env-validator.ts +2 -2
  262. package/cli/src/utils/project-templates.ts +2 -1
  263. package/cli/src/utils/prompt.ts +2 -4
  264. package/cli/src/utils/resource-features.ts +10 -4
  265. package/cli/src/utils/services.ts +2 -1
  266. package/cli/src/utils/tilt.ts +20 -0
  267. package/docs/demo.svg +217 -0
  268. package/engine/assets/docker/wake-gateway/Dockerfile +27 -0
  269. package/engine/assets/docker/wake-gateway/decide.test.ts +133 -0
  270. package/engine/assets/docker/wake-gateway/decide.ts +105 -0
  271. package/engine/assets/docker/wake-gateway/server.ts +311 -0
  272. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
  273. package/engine/topologies/platform/docker/compose/compose.star +7 -14
  274. package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
  275. package/engine/topologies/platform/docker/compose/traefik_standalone.star +66 -6
  276. package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
  277. package/engine/topologies/platform/docker/constants.star +10 -0
  278. package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
  279. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
  280. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +161 -0
  281. package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
  282. package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
  283. package/engine/topologies/tilt/manifest/schema.star +6 -0
  284. package/engine/topologies/tilt/manifest/validator.star +16 -0
  285. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +143 -9
  286. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
  287. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
  288. package/knip.json +1 -1
  289. package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
  290. package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
  291. package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
  292. package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
  293. package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
  294. package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
  295. package/openspec/changes/prioritized-cold-start/design.md +133 -0
  296. package/openspec/changes/prioritized-cold-start/proposal.md +41 -0
  297. package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
  298. package/openspec/changes/prioritized-cold-start/tasks.md +56 -0
  299. package/openspec/config.yaml +1 -0
  300. package/package.json +35 -5
  301. package/scripts/autoresearch/layer-eval.sh +65 -0
  302. package/scripts/benchmark/README.md +25 -0
  303. package/scripts/benchmark/container-scale.ts +503 -0
  304. package/scripts/benchmark/pre-release-gate.sh +9 -0
  305. package/scripts/measure-idle-footprint.sh +58 -0
  306. package/scripts/record-demo.sh +43 -0
  307. package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
  308. package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
  309. package/tests/tilt-engine/test_standalone_traefik.py +52 -0
  310. package/tests/tilt-engine/test_traefik_static_wake_route.py +187 -0
  311. package/bun.lock +0 -538
@@ -0,0 +1,162 @@
1
+ """
2
+ Runtime container footprint: single Bun process, cheap healthchecks, lean golden images.
3
+
4
+ Executes the real Starlark generators through Tilt's interpreter
5
+ (`tilt alpha tiltfile-result`) instead of inspecting source text.
6
+ Spec: openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ pytestmark = [
20
+ pytest.mark.generator,
21
+ pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed"),
22
+ ]
23
+
24
+ DOCKER_DIR = Path(__file__).resolve().parents[2] / "engine" / "topologies" / "platform" / "docker"
25
+ RESULT_MARKER = "Error in fail: RESULT"
26
+
27
+
28
+ def run_starlark(tmp_path: Path, body: str, env: dict | None = None) -> dict:
29
+ """Run `body` in a Tiltfile; `body` must assign a dict to `r`. `@DOCKER` is the engine docker dir."""
30
+ tiltfile = tmp_path / "Tiltfile"
31
+ tiltfile.write_text(
32
+ f"HERE = '{tmp_path}'\n"
33
+ + body.replace("@DOCKER", str(DOCKER_DIR))
34
+ + "\nfail('RESULT' + encode_json(r))\n"
35
+ )
36
+ proc = subprocess.run(
37
+ ["tilt", "alpha", "tiltfile-result", "-f", str(tiltfile)],
38
+ capture_output=True,
39
+ text=True,
40
+ env={**os.environ, **(env or {})},
41
+ timeout=180,
42
+ )
43
+ output = proc.stdout + proc.stderr
44
+ assert RESULT_MARKER in output, output[-3000:]
45
+ return json.loads(output.split(RESULT_MARKER, 1)[1].strip())
46
+
47
+
48
+ def write_service(tmp_path: Path, name: str, start_script: str | None) -> None:
49
+ service_dir = tmp_path / name
50
+ service_dir.mkdir()
51
+ scripts = {} if start_script is None else {"start": start_script}
52
+ (service_dir / "package.json").write_text(json.dumps({"name": name, "scripts": scripts}))
53
+
54
+
55
+ def cmd_lines(dockerfile: str) -> list[str]:
56
+ return [line for line in dockerfile.splitlines() if line.startswith("CMD ")]
57
+
58
+
59
+ def healthcheck_lines(dockerfile: str) -> list[str]:
60
+ return [line for line in dockerfile.splitlines() if line.startswith("HEALTHCHECK")]
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ ("start_script", "expected_cmd"),
65
+ [
66
+ ("bun run dist/index.js", 'CMD ["bun", "dist/index.js"]'),
67
+ ("bun dist/index.js", 'CMD ["bun", "dist/index.js"]'),
68
+ ("bun run dist/index.js --port 4000", 'CMD ["bun", "run", "start"]'),
69
+ ("NODE_ENV=x bun run dist/index.js", 'CMD ["bun", "run", "start"]'),
70
+ ("tsc && bun dist/index.js", 'CMD ["bun", "run", "start"]'),
71
+ ("bun run serve", 'CMD ["bun", "run", "start"]'),
72
+ (None, 'CMD ["bun", "run", "start"]'),
73
+ ],
74
+ )
75
+ def test_backend_cmd_follows_start_script(tmp_path, start_script, expected_cmd):
76
+ write_service(tmp_path, "svc", start_script)
77
+ r = run_starlark(
78
+ tmp_path,
79
+ "load('@DOCKER/layers/l4_runtime_layers.star', 'L4_generate_backend_runtime')\n"
80
+ "r = {\n"
81
+ " 'plain': L4_generate_backend_runtime(HERE + '/svc'),\n"
82
+ " 'infisical': L4_generate_backend_runtime(HERE + '/svc', use_infisical=True, resource_name='svc'),\n"
83
+ "}\n",
84
+ )
85
+ assert cmd_lines(r["plain"]) == [expected_cmd]
86
+ assert cmd_lines(r["infisical"]) == [expected_cmd]
87
+ assert 'ENTRYPOINT ["/entrypoint.sh"]' in r["infisical"]
88
+
89
+
90
+ def test_explicit_cmd_override_is_kept(tmp_path):
91
+ write_service(tmp_path, "svc", "bun run dist/index.js")
92
+ r = run_starlark(
93
+ tmp_path,
94
+ "load('@DOCKER/layers/l4_runtime_layers.star', 'L4_generate_backend_runtime')\n"
95
+ "r = {'df': L4_generate_backend_runtime(HERE + '/svc', cmd='bun run start:prod')}\n",
96
+ )
97
+ assert cmd_lines(r["df"]) == ['CMD ["bun", "run", "start:prod"]']
98
+
99
+
100
+ def test_hugo_installed_only_for_services_declaring_it(tmp_path):
101
+ write_service(tmp_path, "svc", "bun run dist/index.js")
102
+ r = run_starlark(
103
+ tmp_path,
104
+ "load('@DOCKER/layers/l4_runtime_layers.star', 'L4_generate_backend_runtime')\n"
105
+ "r = {\n"
106
+ " 'with': L4_generate_backend_runtime(HERE + '/svc', manifest={'featuresEnabled': ['hugo']}),\n"
107
+ " 'without': L4_generate_backend_runtime(HERE + '/svc', manifest={'featuresEnabled': []}),\n"
108
+ "}\n",
109
+ )
110
+ assert "apk add --no-cache hugo" in r["with"]
111
+ assert "apk add --no-cache hugo" not in r["without"]
112
+
113
+
114
+ def test_golden_dockerfile_is_lean_and_uses_shared_healthcheck(tmp_path):
115
+ r = run_starlark(
116
+ tmp_path,
117
+ "load('@DOCKER/build/golden_image_dockerfile.star', 'generate_golden_dockerfile')\n"
118
+ "r = {'df': generate_golden_dockerfile()}\n",
119
+ )
120
+ df = r["df"]
121
+ backend_bun_stage = df.split("AS l4_backend_bun", 1)[1].split("\nFROM ", 1)[0]
122
+ migrator_stage = df.split("AS l4_migrator_golden", 1)[1].split("\nFROM ", 1)[0]
123
+
124
+ assert "apk add --no-cache hugo" not in backend_bun_stage
125
+ assert "HEALTHCHECK NONE" in migrator_stage
126
+
127
+ checks = healthcheck_lines(df)
128
+ assert checks, "golden Dockerfile has no HEALTHCHECK instructions"
129
+ for line in checks:
130
+ assert "bunx" not in line and "npx" not in line, line
131
+ if line != "HEALTHCHECK NONE":
132
+ assert "--interval=30s --start-interval=2s" in line, line
133
+
134
+
135
+ def test_compose_entries_use_init_and_shared_healthcheck(tmp_path):
136
+ r = run_starlark(
137
+ tmp_path,
138
+ "load('@DOCKER/compose/compose.star', 'generate_backend_compose_entry', 'generate_frontend_compose')\n"
139
+ "r = {\n"
140
+ " 'backend': generate_backend_compose_entry('services/finance', 'finance', {'name': 'cash-api'},"
141
+ " {'stack': 'finance', 'port': 4018, 'appType': 'backend'}),\n"
142
+ " 'frontend': generate_frontend_compose('services/finance', 'finance', {'name': 'dash-app'},"
143
+ " {'stack': 'finance', 'port': 3010, 'appType': 'frontend'}),\n"
144
+ "}\n",
145
+ )
146
+ for kind in ("backend", "frontend"):
147
+ entry = r[kind]
148
+ assert "init: true" in entry, kind
149
+ assert "interval: 30s" in entry, kind
150
+ assert "start_interval: 2s" in entry, kind
151
+ assert "interval: 10s" not in entry, kind
152
+
153
+
154
+ def test_healthcheck_interval_is_configurable(tmp_path):
155
+ r = run_starlark(
156
+ tmp_path,
157
+ "load('@DOCKER/config/healthcheck.star', 'dockerfile_healthcheck_flags', 'compose_healthcheck_timing')\n"
158
+ "r = {'flags': dockerfile_healthcheck_flags(), 'timing': compose_healthcheck_timing()}\n",
159
+ env={"TDK_HEALTHCHECK_INTERVAL_SECONDS": "15"},
160
+ )
161
+ assert "--interval=15s" in r["flags"]
162
+ assert "interval: 15s" in r["timing"]
@@ -0,0 +1,196 @@
1
+ """
2
+ sablier.deferStart: schema field and its enable-required cross-field rule.
3
+
4
+ Executes the real Starlark validator through Tilt's interpreter
5
+ (`tilt alpha tiltfile-result`) instead of inspecting source text.
6
+ Spec: openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ pytestmark = [
20
+ pytest.mark.generator,
21
+ pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed"),
22
+ ]
23
+
24
+ MANIFEST_DIR = Path(__file__).resolve().parents[2] / "engine" / "topologies" / "tilt" / "manifest"
25
+ ORCHESTRATOR_DIR = Path(__file__).resolve().parents[2] / "engine" / "topologies" / "tilt" / "resources" / "orchestrator"
26
+ RESULT_MARKER = "Error in fail: RESULT"
27
+
28
+
29
+ def run_starlark(tmp_path: Path, body: str) -> dict:
30
+ """Run `body` in a Tiltfile; `body` must assign a dict to `r`. `@MANIFEST`/`@ORCHESTRATOR` are module dirs."""
31
+ tiltfile = tmp_path / "Tiltfile"
32
+ tiltfile.write_text(
33
+ body.replace("@MANIFEST", str(MANIFEST_DIR)).replace("@ORCHESTRATOR", str(ORCHESTRATOR_DIR))
34
+ + "\nfail('RESULT' + encode_json(r))\n"
35
+ )
36
+ proc = subprocess.run(
37
+ ["tilt", "alpha", "tiltfile-result", "-f", str(tiltfile)],
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=180,
41
+ )
42
+ output = proc.stdout + proc.stderr
43
+ assert RESULT_MARKER in output, output[-3000:]
44
+ return json.loads(output.split(RESULT_MARKER, 1)[1].strip())
45
+
46
+
47
+ def validate_sablier(tmp_path: Path, sablier: dict | None) -> dict:
48
+ manifest = {"appType": "backend", "stack": "test"}
49
+ if sablier is not None:
50
+ manifest["sablier"] = sablier
51
+ return run_starlark(
52
+ tmp_path,
53
+ "load('@MANIFEST/validator.star', 'ManifestValidator')\n"
54
+ f"manifest = {manifest!r}\n"
55
+ "result = ManifestValidator.validate(manifest, level='cross_field')\n"
56
+ "r = {'valid': result.valid, 'errors': [e.message for e in result.errors]}\n",
57
+ )
58
+
59
+
60
+ @pytest.mark.parametrize(
61
+ "sablier",
62
+ [
63
+ None,
64
+ {"enable": False},
65
+ {"enable": True},
66
+ {"enable": True, "deferStart": True},
67
+ {"enable": True, "deferStart": False},
68
+ ],
69
+ )
70
+ def test_defer_start_with_enable_is_valid(tmp_path, sablier):
71
+ r = validate_sablier(tmp_path, sablier)
72
+ assert r["valid"], r["errors"]
73
+
74
+
75
+ def test_defer_start_without_enable_is_rejected(tmp_path):
76
+ r = validate_sablier(tmp_path, {"deferStart": True})
77
+ assert not r["valid"]
78
+ assert any("deferStart" in e and "enable" in e for e in r["errors"]), r["errors"]
79
+
80
+
81
+ def test_defer_start_with_enable_false_is_rejected(tmp_path):
82
+ r = validate_sablier(tmp_path, {"enable": False, "deferStart": True})
83
+ assert not r["valid"]
84
+ assert any("deferStart" in e and "enable" in e for e in r["errors"]), r["errors"]
85
+
86
+
87
+ def resource_defers_start(tmp_path: Path, sablier: dict | None) -> bool:
88
+ manifest = {"appType": "backend", "stack": "test"}
89
+ if sablier is not None:
90
+ manifest["sablier"] = sablier
91
+ result = run_starlark(
92
+ tmp_path,
93
+ "load('@ORCHESTRATOR/apply_compose_resource_registration.star', 'resource_defers_start')\n"
94
+ f"manifest = {manifest!r}\n"
95
+ "r = {'defers': resource_defers_start(manifest)}\n",
96
+ )
97
+ return result["defers"]
98
+
99
+
100
+ @pytest.mark.parametrize(
101
+ "sablier",
102
+ [
103
+ None,
104
+ {},
105
+ {"enable": False},
106
+ {"enable": True},
107
+ {"enable": False, "deferStart": True},
108
+ {"enable": True, "deferStart": False},
109
+ {"enable": True, "deferStart": True},
110
+ ],
111
+ )
112
+ def test_resource_defers_start_stays_off_without_a_license(tmp_path, sablier):
113
+ """deferStart stays behind the same TDK_LICENSE_KEY gate as the rest of
114
+ `sablier` (an explicit product decision -- see openspec/changes/
115
+ prioritized-cold-start's design.md). This repo ships only the free-tier
116
+ `sablier_container_cycle.star` stub, whose sablier_middleware_suffix()
117
+ always reports disabled regardless of the manifest -- so every case here
118
+ must resolve to False, including {"enable": True, "deferStart": True}.
119
+ The corresponding "licensed and enabled" True case can only be exercised
120
+ where the real premium module is actually vendored (tdk-cli-extensions),
121
+ not from this repo's stub."""
122
+ assert resource_defers_start(tmp_path, sablier) is False
123
+
124
+
125
+ def _run_resolve_deps(tmp_path: Path, manifest: dict, resource_manifests: dict, all_services_map: dict) -> list:
126
+ result = run_starlark(
127
+ tmp_path,
128
+ "load('@ORCHESTRATOR/apply_compose_resource_registration.star', 'resolve_defer_start_dependencies')\n"
129
+ f"manifest = {manifest!r}\n"
130
+ f"resource_manifests = {resource_manifests!r}\n"
131
+ f"all_services_map = {all_services_map!r}\n"
132
+ "r = {'deps': [list(pair) for pair in resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map)]}\n",
133
+ )
134
+ return result["deps"]
135
+
136
+
137
+ LICENSED_SABLIER_MANIFEST = {"appType": "backend", "stack": "test", "sablier": {"enable": True, "deferStart": True}}
138
+
139
+
140
+ def test_resolve_defer_start_dependencies_excludes_non_defer_dependency(tmp_path):
141
+ """A dependsOn entry that resolves to a real resource with no sablier.deferStart
142
+ (e.g. always-on infra, or a normal backend) contributes nothing to X-Wake-Deps."""
143
+ manifest = {"appType": "backend", "stack": "test", "dependsOn": ["plain-backend"]}
144
+ resource_manifests = {"plain-backend": {"appType": "backend", "stack": "other"}}
145
+ all_services_map = {"plain-backend": {"name": "other-service", "resources": [{"name": "plain-backend"}]}}
146
+ assert _run_resolve_deps(tmp_path, manifest, resource_manifests, all_services_map) == []
147
+
148
+
149
+ def test_resolve_defer_start_dependencies_excludes_unresolvable_name(tmp_path):
150
+ """A dependsOn short name that doesn't match any known resource/service falls
151
+ back (via _resolve_dependency_to_resource) to a name absent from
152
+ resource_manifests, so it's quietly excluded rather than guessed at (D5)."""
153
+ manifest = {"appType": "backend", "stack": "test", "dependsOn": ["nonexistent-thing"]}
154
+ assert _run_resolve_deps(tmp_path, manifest, {}, {}) == []
155
+
156
+
157
+ def test_resolve_defer_start_dependencies_stays_empty_without_a_license(tmp_path):
158
+ """Even when a dependsOn entry resolves to a resource whose manifest asks for
159
+ sablier.deferStart, this repo's free-tier stub keeps resource_defers_start()
160
+ False for it too (same license gate as the resource's own deferStart check),
161
+ so it's excluded here just as it would be for the resource itself."""
162
+ manifest = {"appType": "backend", "stack": "test", "dependsOn": ["peer-backend"]}
163
+ resource_manifests = {"peer-backend": LICENSED_SABLIER_MANIFEST}
164
+ all_services_map = {"peer-backend": {"name": "peer-service", "resources": [{"name": "peer-backend"}]}}
165
+ assert _run_resolve_deps(tmp_path, manifest, resource_manifests, all_services_map) == []
166
+
167
+
168
+ def test_resolve_defer_start_dependencies_no_depends_on(tmp_path):
169
+ manifest = {"appType": "backend", "stack": "test"}
170
+ assert _run_resolve_deps(tmp_path, manifest, {}, {}) == []
171
+
172
+
173
+ def _run_resolve_names(tmp_path: Path, manifest: dict, all_services_map: dict) -> list:
174
+ result = run_starlark(
175
+ tmp_path,
176
+ "load('@ORCHESTRATOR/apply_compose_resource_registration.star', 'resolve_manifest_dependency_names')\n"
177
+ f"manifest = {manifest!r}\n"
178
+ f"all_services_map = {all_services_map!r}\n"
179
+ "r = {'names': resolve_manifest_dependency_names(manifest, all_services_map)}\n",
180
+ )
181
+ return result["names"]
182
+
183
+
184
+ def test_resolve_manifest_dependency_names_exact_match(tmp_path):
185
+ """The common case: dependsOn already names a real resource exactly, so
186
+ _resolve_dependency_to_resource returns it unchanged (no '-yaml' guess)."""
187
+ manifest = {"dependsOn": ["identity-management-backend"]}
188
+ all_services_map = {
189
+ "identity-management-backend": {"name": "identity-management", "resources": [{"name": "identity-management-backend"}]},
190
+ }
191
+ assert _run_resolve_names(tmp_path, manifest, all_services_map) == ["identity-management-backend"]
192
+
193
+
194
+ def test_resolve_manifest_dependency_names_dedupes_and_skips_empty(tmp_path):
195
+ manifest = {"dependsOn": []}
196
+ assert _run_resolve_names(tmp_path, manifest, {}) == []
@@ -1,5 +1,8 @@
1
1
  """Standalone TDK projects must serve api.{project}.localhost via Traefik."""
2
2
 
3
+ import json
4
+ import shutil
5
+ import subprocess
3
6
  from pathlib import Path
4
7
 
5
8
  import pytest
@@ -7,6 +10,22 @@ import pytest
7
10
  pytestmark = [pytest.mark.fast, pytest.mark.generator]
8
11
 
9
12
  REPO_ROOT = Path(__file__).resolve().parents[2]
13
+ COMPOSE_DIR = REPO_ROOT / "engine" / "topologies" / "platform" / "docker" / "compose"
14
+ RESULT_MARKER = "Error in fail: RESULT"
15
+
16
+
17
+ def _run_starlark(tmp_path: Path, body: str) -> dict:
18
+ tiltfile = tmp_path / "Tiltfile"
19
+ tiltfile.write_text(body.replace("@COMPOSE", str(COMPOSE_DIR)) + "\nfail('RESULT' + encode_json(r))\n")
20
+ proc = subprocess.run(
21
+ ["tilt", "alpha", "tiltfile-result", "-f", str(tiltfile)],
22
+ capture_output=True,
23
+ text=True,
24
+ timeout=180,
25
+ )
26
+ output = proc.stdout + proc.stderr
27
+ assert RESULT_MARKER in output, output[-3000:]
28
+ return json.loads(output.split(RESULT_MARKER, 1)[1].strip())
10
29
 
11
30
 
12
31
  def test_standalone_traefik_compose_generator_exists():
@@ -106,3 +125,36 @@ def test_traefik_conflict_takeover_stops_other_project_and_logs_it():
106
125
  # instead of just erroring out.
107
126
  assert "docker stop" in stop_body
108
127
  assert "print(" in stop_body
128
+
129
+
130
+ @pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed")
131
+ def test_standalone_compose_enables_file_provider_and_wake_gateway(tmp_path):
132
+ """openspec/changes/prioritized-cold-start (D4): a `sablier.deferStart`
133
+ resource's static route needs Traefik's file provider enabled and a wake
134
+ gateway running, even in a landscape with no such resource yet (harmless:
135
+ an empty dynamic directory yields zero extra routes)."""
136
+ r = _run_starlark(
137
+ tmp_path,
138
+ "load('@COMPOSE/traefik_standalone.star', 'generate_standalone_traefik_compose')\n"
139
+ "r = {'yaml': generate_standalone_traefik_compose()}\n",
140
+ )
141
+ yaml_text = r["yaml"]
142
+ assert "--providers.file.directory=/etc/traefik/dynamic" in yaml_text
143
+ assert "--providers.file.watch=true" in yaml_text
144
+ assert "/etc/traefik/dynamic:ro" in yaml_text
145
+ assert "wake-gateway:" in yaml_text
146
+ assert "/root/.tilt-dev:ro" in yaml_text
147
+
148
+
149
+ @pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed")
150
+ def test_wake_gateway_mounts_project_root_at_its_own_absolute_path(tmp_path):
151
+ """The gateway runs `docker compose up --no-build` with the same compose
152
+ files Tilt uses; relative paths inside them resolve client-side, so the
153
+ project must exist at the same absolute path in the gateway container."""
154
+ r = _run_starlark(
155
+ tmp_path,
156
+ "os.environ['TDK_PROJECT_ROOT'] = '/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'\n"
157
+ "load('@COMPOSE/traefik_standalone.star', 'generate_standalone_traefik_compose')\n"
158
+ "r = {'yaml': generate_standalone_traefik_compose()}\n",
159
+ )
160
+ assert "- /Users/dev/tdk-erp-system:/Users/dev/tdk-erp-system:ro" in r["yaml"]
@@ -0,0 +1,187 @@
1
+ """
2
+ Traefik static (file-provider) wake route for `sablier.deferStart` resources.
3
+
4
+ Executes the real Starlark generators through Tilt's interpreter
5
+ (`tilt alpha tiltfile-result`) instead of inspecting source text.
6
+ Spec: openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md
7
+ Design: openspec/changes/prioritized-cold-start/design.md (D4)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ import pytest
19
+
20
+ pytestmark = [
21
+ pytest.mark.generator,
22
+ pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed"),
23
+ ]
24
+
25
+ NETWORKING_DIR = Path(__file__).resolve().parents[2] / "engine" / "topologies" / "platform" / "docker" / "networking"
26
+ RESULT_MARKER = "Error in fail: RESULT"
27
+
28
+
29
+ def run_starlark(tmp_path: Path, body: str) -> dict:
30
+ tiltfile = tmp_path / "Tiltfile"
31
+ tiltfile.write_text(body.replace("@NETWORKING", str(NETWORKING_DIR)) + "\nfail('RESULT' + encode_json(r))\n")
32
+ proc = subprocess.run(
33
+ ["tilt", "alpha", "tiltfile-result", "-f", str(tiltfile)],
34
+ capture_output=True,
35
+ text=True,
36
+ timeout=180,
37
+ )
38
+ output = proc.stdout + proc.stderr
39
+ assert RESULT_MARKER in output, output[-3000:]
40
+ return json.loads(output.split(RESULT_MARKER, 1)[1].strip())
41
+
42
+
43
+ MANIFEST = {
44
+ "appType": "backend",
45
+ "stack": "analytics",
46
+ "sablier": {"enable": True, "deferStart": True},
47
+ }
48
+
49
+
50
+ def test_static_route_rule_matches_docker_label_project_router(tmp_path):
51
+ r = run_starlark(
52
+ tmp_path,
53
+ "load('@NETWORKING/traefik.star', 'get_backend_traefik_labels')\n"
54
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
55
+ f"manifest = {MANIFEST!r}\n"
56
+ "docker_labels = get_backend_traefik_labels(\n"
57
+ " 'data-governance-api', 'analytics.backend.tdk_erp_system.local', '',\n"
58
+ " 'data-governance-api', 4604, '/health', manifest=manifest,\n"
59
+ ")\n"
60
+ "static_yaml = generate_static_wake_route(\n"
61
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
62
+ " 'tdk-wake-gateway', 9999,\n"
63
+ " dependencies=[('postgres', 'analytics-autogenerated')],\n"
64
+ ")\n"
65
+ "r = {'docker_labels': docker_labels, 'static_yaml': static_yaml}\n",
66
+ )
67
+
68
+ docker_rule_match = re.search(
69
+ r'traefik\.http\.routers\.data-governance-api-project\.rule=(.+?)"', r["docker_labels"]
70
+ )
71
+ docker_priority_match = re.search(
72
+ r'traefik\.http\.routers\.data-governance-api-project\.priority=(\d+)"', r["docker_labels"]
73
+ )
74
+ assert docker_rule_match and docker_priority_match
75
+
76
+ static_rule_match = re.search(r'rule:\s*"(.+?)"', r["static_yaml"])
77
+ static_priority_match = re.search(r"priority:\s*(\d+)", r["static_yaml"])
78
+ assert static_rule_match and static_priority_match
79
+
80
+ # Same routing rule as the Docker-label path (task 4.2)...
81
+ assert static_rule_match.group(1) == docker_rule_match.group(1)
82
+ # ...but a fixed low priority, not the computed one (task 1.4's finding:
83
+ # at equal priority the file-provider router won even over a healthy
84
+ # Docker-discovered backend).
85
+ assert int(static_priority_match.group(1)) == 1
86
+ assert int(static_priority_match.group(1)) < int(docker_priority_match.group(1))
87
+
88
+
89
+ def test_static_route_headers_carry_container_names_not_just_resource_names(tmp_path):
90
+ r = run_starlark(
91
+ tmp_path,
92
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
93
+ f"manifest = {MANIFEST!r}\n"
94
+ "r = {'yaml': generate_static_wake_route(\n"
95
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
96
+ " 'tdk-wake-gateway', 9999,\n"
97
+ " dependencies=[('postgres', 'analytics-autogenerated')],\n"
98
+ ")}\n",
99
+ )
100
+ yaml = r["yaml"]
101
+ assert 'X-Wake-Resource: "data-governance-api"' in yaml
102
+ # Container name follows Compose's own <project>-<service>-<replica>
103
+ # convention, confirmed empirically against a live TDK container.
104
+ assert 'X-Wake-Container: "analytics-autogenerated-data-governance-api-1"' in yaml
105
+ assert 'X-Wake-Deps: "postgres=analytics-autogenerated-postgres-1"' in yaml
106
+ assert 'X-Wake-Proxy-Port: "4604"' in yaml
107
+
108
+
109
+ def test_static_route_strips_the_same_api_path_prefix_the_docker_router_does(tmp_path):
110
+ """Found live against tdk-erp-system (task 7.2): the app itself only
111
+ serves its bare health path (e.g. `/health`), not the full
112
+ `/api/data-governance/health` the public route matches on -- the real
113
+ Docker-label `-project` router strips that prefix (traefik.star), and
114
+ the wake route's own proxy-through must do the same or a woken resource
115
+ 404s even though the wake itself succeeded."""
116
+ r = run_starlark(
117
+ tmp_path,
118
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
119
+ f"manifest = {MANIFEST!r}\n"
120
+ "r = {'yaml': generate_static_wake_route(\n"
121
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
122
+ " 'tdk-wake-gateway', 9999,\n"
123
+ ")}\n",
124
+ )
125
+ yaml = r["yaml"]
126
+ assert "data-governance-api-wake-strip" in yaml
127
+ assert 'prefixes:\n - "/api/data-governance"' in yaml
128
+ # Both middlewares must actually be attached to the router, in strip-then-headers order.
129
+ strip_idx = yaml.index("- data-governance-api-wake-strip")
130
+ headers_idx = yaml.index("- data-governance-api-wake-headers")
131
+ assert strip_idx < headers_idx
132
+
133
+
134
+ def test_static_route_with_no_dependencies_omits_deps_header_value(tmp_path):
135
+ r = run_starlark(
136
+ tmp_path,
137
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
138
+ f"manifest = {MANIFEST!r}\n"
139
+ "r = {'yaml': generate_static_wake_route(\n"
140
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
141
+ " 'tdk-wake-gateway', 9999,\n"
142
+ ")}\n",
143
+ )
144
+ assert 'X-Wake-Deps: ""' in r["yaml"]
145
+ # No compose details passed -> the gateway can only use `tilt trigger`.
146
+ assert "X-Wake-Compose-" not in r["yaml"]
147
+ assert "X-Wake-Image" not in r["yaml"]
148
+
149
+
150
+ def test_normalize_abs_path_collapses_dot_segments(tmp_path):
151
+ """Tilt's os.path.abspath leaves `..` in place; the generated Tiltfile's
152
+ project root is config.main_dir + '/../..', so this must collapse it."""
153
+ r = run_starlark(
154
+ tmp_path,
155
+ "load('@NETWORKING/traefik_static_routes.star', 'normalize_abs_path')\n"
156
+ "r = {\n"
157
+ " 'root': normalize_abs_path('/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'),\n"
158
+ " 'dot': normalize_abs_path('/a/./b//c/'),\n"
159
+ " 'empty': normalize_abs_path(''),\n"
160
+ "}\n",
161
+ )
162
+ assert r == {"root": "/Users/dev/tdk-erp-system", "dot": "/a/b/c", "empty": ""}
163
+
164
+
165
+ def test_static_route_carries_compose_invocation_for_direct_start(tmp_path):
166
+ """With compose details, the gateway can `docker compose up --no-build` the
167
+ resource itself instead of waiting in Tilt's build queue (task 7.2 finding)."""
168
+ root = "/Users/dev/tdk-erp-system"
169
+ compose_file = root + "/services/analytics/.autogenerated/docker-compose.app.autogenerated.yml"
170
+ override_file = root + "/services/analytics/.autogenerated/docker-compose.port-override.yml"
171
+ r = run_starlark(
172
+ tmp_path,
173
+ "load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
174
+ f"manifest = {MANIFEST!r}\n"
175
+ "r = {'yaml': generate_static_wake_route(\n"
176
+ " 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
177
+ " 'tdk-wake-gateway', 9999,\n"
178
+ f" compose_files=[{compose_file!r}, {override_file!r}],\n"
179
+ f" env_file={root + '/.env'!r},\n"
180
+ " image='analytics_data-governance-api:dev',\n"
181
+ ")}\n",
182
+ )
183
+ yaml = r["yaml"]
184
+ assert 'X-Wake-Compose-Project: "analytics-autogenerated"' in yaml
185
+ assert f'X-Wake-Compose-Files: "{compose_file},{override_file}"' in yaml
186
+ assert f'X-Wake-Env-File: "{root}/.env"' in yaml
187
+ assert 'X-Wake-Image: "analytics_data-governance-api:dev"' in yaml