thinkwork-cli 0.13.2 → 1.0.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 (231) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
  3. package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
  4. package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
  5. package/dist/chunk-MWQ7RO76.js +137 -0
  6. package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
  7. package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
  8. package/dist/cli.js +4400 -16431
  9. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  11. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  12. package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
  13. package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
  14. package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
  15. package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
  16. package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
  17. package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
  18. package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
  19. package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
  20. package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
  21. package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
  22. package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
  23. package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
  24. package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
  25. package/dist/drizzle/0230_analyst_rls.sql +28 -45
  26. package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
  27. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  28. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  29. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  30. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  31. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  32. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  33. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  34. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  35. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  36. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  37. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  38. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  39. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  40. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  41. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  42. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  43. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  44. package/dist/drizzle/0287_brain_subject.sql +25 -0
  45. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  46. package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
  47. package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
  48. package/dist/plugins/README.md +2 -2
  49. package/dist/plugins/catalog/package.json +0 -3
  50. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
  51. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  52. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
  53. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
  54. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
  55. package/dist/plugins/catalog/src/contracts.ts +15 -0
  56. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
  57. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  58. package/dist/plugins/twenty/package.json +2 -1
  59. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  60. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  61. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  62. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  63. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  64. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  65. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  66. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  67. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  68. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  69. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  70. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  71. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  72. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  73. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  74. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  75. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  76. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  77. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  78. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  79. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  80. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  81. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  82. package/dist/scripts/post-deploy.sh +79 -1
  83. package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
  84. package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
  85. package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
  86. package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
  87. package/dist/terraform/examples/greenfield/main.tf +233 -175
  88. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
  89. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  90. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  91. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  92. package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
  93. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
  100. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
  101. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  102. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  103. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  104. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  105. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  106. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  107. package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
  108. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  109. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  110. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  111. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  112. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  113. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  114. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  115. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  116. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  117. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  118. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  119. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  120. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  121. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  122. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  123. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  124. package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
  125. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
  126. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  127. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  128. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  129. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  130. package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
  131. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
  132. package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
  133. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
  134. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  135. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  136. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
  137. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  138. package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
  139. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  140. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  141. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  142. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  143. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  144. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  145. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  146. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  147. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  148. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  149. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  150. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  151. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  152. package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
  153. package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
  154. package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
  155. package/dist/terraform/modules/thinkwork/README.md +5 -1
  156. package/dist/terraform/modules/thinkwork/main.tf +376 -374
  157. package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
  158. package/dist/terraform/modules/thinkwork/variables.tf +246 -89
  159. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  160. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  161. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  162. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  163. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  164. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  165. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  166. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  167. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  168. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  169. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  171. package/package.json +1 -1
  172. package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
  173. package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
  174. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
  175. package/dist/plugins/company-data/README.md +0 -49
  176. package/dist/plugins/company-data/package.json +0 -21
  177. package/dist/plugins/company-data/src/index.ts +0 -30
  178. package/dist/plugins/company-data/src/manifest.ts +0 -26
  179. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  180. package/dist/plugins/company-data/tsconfig.json +0 -8
  181. package/dist/plugins/company-etl/README.md +0 -46
  182. package/dist/plugins/company-etl/package.json +0 -21
  183. package/dist/plugins/company-etl/src/index.ts +0 -29
  184. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  185. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  186. package/dist/plugins/company-etl/tsconfig.json +0 -8
  187. package/dist/plugins/lastmile/README.md +0 -50
  188. package/dist/plugins/lastmile/package.json +0 -23
  189. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  190. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  191. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  192. package/dist/plugins/lastmile/src/index.ts +0 -46
  193. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  194. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  195. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  196. package/dist/plugins/lastmile/tsconfig.json +0 -8
  197. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  198. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  199. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  200. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  201. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  202. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  203. package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
  204. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
  205. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
  206. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
  207. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
  208. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
  209. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
  210. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
  211. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
  212. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
  213. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
  214. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
  215. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
  216. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
  217. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
  218. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
  219. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  220. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  221. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
  222. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
  223. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  224. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  225. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  226. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  227. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  228. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  229. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  230. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  231. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -142,6 +142,269 @@ def test_customer_update_accepts_customer_ecr_agentcore_pi_source_image() -> Non
142
142
  )
143
143
 
144
144
 
145
+ SECRET_PEM = "-----BEGIN PUBLIC KEY-----\nMIIFakePemBody\n-----END PUBLIC KEY-----"
146
+ SECRET_API = "fake-api-auth-secret-000"
147
+ PI_RECONCILE_PAYLOAD = {
148
+ "action": "update",
149
+ "awsAccountId": "637423202447",
150
+ "awsRegion": "us-east-1",
151
+ # The runtime reconcile resolves the arm64 mirror by release version tag
152
+ # (AgentCore is arm64-only); the amd64 Lambda pin below is NOT used for
153
+ # the runtime image (tei-e2e canary, 2026-08-04).
154
+ "releaseVersion": "v0.1.0-canary.434",
155
+ "agentcorePiSourceImageUri": (
156
+ "637423202447.dkr.ecr.us-east-1.amazonaws.com/thinkwork-tei-e2e-agentcore:pinned"
157
+ ),
158
+ }
159
+ PI_DIGEST = "sha256:" + "a" * 64
160
+
161
+
162
+ def _pi_reconcile_fixture(runner, tmp_path, monkeypatch, ssm_runtime_id):
163
+ entry_dir = tmp_path / "control-runtime"
164
+ entry_dir.mkdir()
165
+ (entry_dir / "reconcile_pi_runtime.js").write_text("// stub", encoding="utf-8")
166
+ monkeypatch.setenv("THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR", str(entry_dir))
167
+
168
+ calls = {"output": [], "run": [], "node": []}
169
+
170
+ def fake_output(args, **kwargs):
171
+ calls["output"].append(args)
172
+ if args[1] == "lambda":
173
+ return json.dumps(
174
+ {
175
+ "Role": "arn:aws:iam::637423202447:role/thinkwork-tei-e2e-agentcore-pi-role",
176
+ "FunctionArn": (
177
+ "arn:aws:lambda:us-east-1:637423202447:function:thinkwork-tei-e2e-agentcore-pi"
178
+ ),
179
+ "Environment": {
180
+ "Variables": {
181
+ "CAPABILITY_SIGNING_PUBLIC_KEY": SECRET_PEM,
182
+ "API_AUTH_SECRET": SECRET_API,
183
+ }
184
+ },
185
+ }
186
+ )
187
+ if args[1] == "ecr":
188
+ return PI_DIGEST
189
+ if args[1] == "ssm":
190
+ if ssm_runtime_id is None:
191
+ raise subprocess.CalledProcessError(255, args)
192
+ return ssm_runtime_id
193
+ raise AssertionError(f"unexpected output() args: {args}")
194
+
195
+ def fake_run(args, **kwargs):
196
+ calls["run"].append(args)
197
+ return SimpleNamespace(returncode=0)
198
+
199
+ def fake_subprocess_run(args, **kwargs):
200
+ calls["node"].append({"args": args, "input": kwargs.get("input", "")})
201
+ return SimpleNamespace(
202
+ returncode=0,
203
+ stdout=json.dumps(
204
+ {
205
+ "runtimeId": "rt-pi-123",
206
+ "created": ssm_runtime_id is None,
207
+ "version": "7",
208
+ "status": "READY",
209
+ "envCount": 2,
210
+ }
211
+ ),
212
+ stderr="",
213
+ )
214
+
215
+ monkeypatch.setattr(runner, "output", fake_output)
216
+ monkeypatch.setattr(runner, "run", fake_run)
217
+ monkeypatch.setattr(runner.subprocess, "run", fake_subprocess_run)
218
+ return calls
219
+
220
+
221
+ def test_pi_runtime_reconcile_creates_when_absent_and_writes_ssm(
222
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
223
+ ) -> None:
224
+ runner = load_runner()
225
+ calls = _pi_reconcile_fixture(runner, tmp_path, monkeypatch, ssm_runtime_id=None)
226
+
227
+ result = runner.reconcile_agentcore_pi_runtime({"stage": "tei-e2e"}, PI_RECONCILE_PAYLOAD)
228
+
229
+ assert result["runtimeId"] == "rt-pi-123"
230
+ request = json.loads(calls["node"][0]["input"])
231
+ assert request["runtimeId"] == ""
232
+ # AgentCore names forbid hyphens ([a-zA-Z][a-zA-Z0-9_]{0,47}) — the
233
+ # tei-e2e canary proved the unsanitized name 400s (2026-08-04).
234
+ assert request["runtimeName"] == "thinkwork_tei_e2e_pi"
235
+ # The runtime image resolves from the versioned arm64 mirror tag, never
236
+ # from the amd64 Lambda pin.
237
+ ecr_calls = [args for args in calls["output"] if args[1] == "ecr"]
238
+ assert any("imageTag=v0.1.0-canary.434-pi-arm64" in " ".join(args) for args in ecr_calls)
239
+ assert request["imageUri"].endswith(f"@{PI_DIGEST}")
240
+ assert request["environment"]["API_AUTH_SECRET"] == SECRET_API
241
+ assert "\\n" in request["environment"]["CAPABILITY_SIGNING_PUBLIC_KEY"]
242
+ assert "\n" not in request["environment"]["CAPABILITY_SIGNING_PUBLIC_KEY"]
243
+ ssm_put = [args for args in calls["run"] if args[2] == "put-parameter"]
244
+ assert len(ssm_put) == 1 and "rt-pi-123" in ssm_put[0]
245
+ # R19: secret values never reach argv or logs.
246
+ for args in calls["node"]:
247
+ assert SECRET_API not in " ".join(args["args"])
248
+ captured = capsys.readouterr()
249
+ assert SECRET_API not in captured.out + captured.err
250
+ assert "MIIFakePemBody" not in captured.out + captured.err
251
+
252
+
253
+ def test_pi_runtime_reconcile_updates_when_present(
254
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
255
+ ) -> None:
256
+ runner = load_runner()
257
+ calls = _pi_reconcile_fixture(runner, tmp_path, monkeypatch, ssm_runtime_id="rt-pi-123")
258
+
259
+ result = runner.reconcile_agentcore_pi_runtime({"stage": "tei-e2e"}, PI_RECONCILE_PAYLOAD)
260
+
261
+ assert result["runtimeId"] == "rt-pi-123"
262
+ request = json.loads(calls["node"][0]["input"])
263
+ assert request["runtimeId"] == "rt-pi-123"
264
+ assert [args for args in calls["run"] if args[2] == "put-parameter"]
265
+
266
+
267
+ def test_pi_runtime_reconcile_aborts_on_empty_lambda_env(
268
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
269
+ ) -> None:
270
+ runner = load_runner()
271
+ calls = _pi_reconcile_fixture(runner, tmp_path, monkeypatch, ssm_runtime_id="rt-pi-123")
272
+
273
+ def empty_env_output(args, **kwargs):
274
+ if args[1] == "lambda":
275
+ return json.dumps(
276
+ {
277
+ "Role": "arn:aws:iam::637423202447:role/r",
278
+ "FunctionArn": "arn:aws:lambda:us-east-1:637423202447:function:f",
279
+ "Environment": {"Variables": {}},
280
+ }
281
+ )
282
+ raise AssertionError("must abort before any non-lambda call")
283
+
284
+ monkeypatch.setattr(runner, "output", empty_env_output)
285
+
286
+ with pytest.raises(RuntimeError, match="EMPTY environment"):
287
+ runner.reconcile_agentcore_pi_runtime({"stage": "tei-e2e"}, PI_RECONCILE_PAYLOAD)
288
+ assert not calls["node"]
289
+
290
+
291
+ def test_pi_runtime_reconcile_skips_on_legacy_bundle(
292
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
293
+ ) -> None:
294
+ runner = load_runner()
295
+ monkeypatch.delenv("THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR", raising=False)
296
+
297
+ assert runner.reconcile_agentcore_pi_runtime({"stage": "tei-e2e"}, PI_RECONCILE_PAYLOAD) is None
298
+ assert "SKIPPED" in capsys.readouterr().out
299
+
300
+
301
+ def test_native_auth_custom_attributes_skip_before_user_pool_exists(
302
+ monkeypatch: pytest.MonkeyPatch,
303
+ ) -> None:
304
+ runner = load_runner()
305
+ monkeypatch.setattr(
306
+ runner,
307
+ "output",
308
+ lambda *_args, **_kwargs: pytest.fail("Cognito should not be queried"),
309
+ )
310
+
311
+ assert runner.ensure_native_auth_custom_attributes({}) == {
312
+ "status": "skipped",
313
+ "reason": "user-pool-not-created",
314
+ }
315
+
316
+
317
+ def test_native_auth_custom_attributes_are_added_before_route_clients(
318
+ monkeypatch: pytest.MonkeyPatch,
319
+ ) -> None:
320
+ runner = load_runner()
321
+ descriptions = iter(
322
+ [
323
+ json.dumps(
324
+ {
325
+ "UserPool": {
326
+ "SchemaAttributes": [
327
+ {"Name": "email"},
328
+ {"Name": "custom:tenant_id"},
329
+ ]
330
+ }
331
+ }
332
+ ),
333
+ json.dumps(
334
+ {
335
+ "UserPool": {
336
+ "SchemaAttributes": [
337
+ {"Name": "email"},
338
+ {"Name": "custom:tenant_id"},
339
+ {"Name": "custom:entra_tenant_id"},
340
+ {"Name": "custom:entra_object_id"},
341
+ ]
342
+ }
343
+ }
344
+ ),
345
+ ]
346
+ )
347
+ calls = []
348
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: next(descriptions))
349
+ monkeypatch.setattr(
350
+ runner,
351
+ "run",
352
+ lambda args, **kwargs: calls.append((args, kwargs)),
353
+ )
354
+
355
+ result = runner.ensure_native_auth_custom_attributes(
356
+ {"user_pool_id": {"value": "us-east-1_Example123"}}
357
+ )
358
+
359
+ assert result == {
360
+ "status": "reconciled",
361
+ "userPoolId": "us-east-1_Example123",
362
+ "added": ["entra_tenant_id", "entra_object_id"],
363
+ }
364
+ assert len(calls) == 1
365
+ command, kwargs = calls[0]
366
+ assert command[:3] == ["aws", "cognito-idp", "add-custom-attributes"]
367
+ request = json.loads(command[command.index("--cli-input-json") + 1])
368
+ assert request == {
369
+ "UserPoolId": "us-east-1_Example123",
370
+ "CustomAttributes": runner.NATIVE_AUTH_CUSTOM_ATTRIBUTES,
371
+ }
372
+ assert kwargs == {"stdout": subprocess.DEVNULL}
373
+
374
+
375
+ def test_native_auth_custom_attributes_are_idempotent(
376
+ monkeypatch: pytest.MonkeyPatch,
377
+ ) -> None:
378
+ runner = load_runner()
379
+ monkeypatch.setattr(
380
+ runner,
381
+ "output",
382
+ lambda *_args, **_kwargs: json.dumps(
383
+ {
384
+ "UserPool": {
385
+ "SchemaAttributes": [
386
+ {"Name": "custom:entra_tenant_id"},
387
+ {"Name": "custom:entra_object_id"},
388
+ ]
389
+ }
390
+ }
391
+ ),
392
+ )
393
+ monkeypatch.setattr(
394
+ runner,
395
+ "run",
396
+ lambda *_args, **_kwargs: pytest.fail("No mutation should be required"),
397
+ )
398
+
399
+ assert runner.ensure_native_auth_custom_attributes(
400
+ {"user_pool_id": {"value": "us-east-1_Example123"}}
401
+ ) == {
402
+ "status": "current",
403
+ "userPoolId": "us-east-1_Example123",
404
+ "added": [],
405
+ }
406
+
407
+
145
408
  def test_native_auth_reconciliation_builds_route_specific_secret_free_manifest(
146
409
  monkeypatch: pytest.MonkeyPatch,
147
410
  ) -> None:
@@ -226,6 +489,54 @@ def test_native_auth_reconciliation_builds_route_specific_secret_free_manifest(
226
489
  assert replay_state == state
227
490
 
228
491
 
492
+ def _microsoft_reconciliation_scope(tenant: str) -> tuple[dict, dict]:
493
+ outputs = {
494
+ "user_pool_id": {"value": "us-east-1_TESTPOOL"},
495
+ "auth_route_clients": {
496
+ "value": {
497
+ "web:microsoft": {
498
+ "client_id": "zyxwvutsrqponmlkjihgfedcba",
499
+ "route_key": "microsoft",
500
+ "client_family": "web",
501
+ "provider_names": ["MicrosoftOrganizations"],
502
+ "explicit_auth_flows": ["ALLOW_REFRESH_TOKEN_AUTH"],
503
+ "callback_urls": ["https://app.example/auth/callback"],
504
+ "logout_urls": ["https://app.example"],
505
+ "lifecycle_state": "native",
506
+ },
507
+ }
508
+ },
509
+ }
510
+ scope = {
511
+ "stage": "dev",
512
+ "account_id": "123456789012",
513
+ "region": "us-east-1",
514
+ "microsoft_oauth_tenant": tenant,
515
+ }
516
+ return outputs, scope
517
+
518
+
519
+ @pytest.mark.parametrize("alias", ["common", "organizations", "consumers"])
520
+ def test_native_auth_reconciliation_accepts_cognito_tenant_aliases(alias: str) -> None:
521
+ runner = load_runner()
522
+ outputs, scope = _microsoft_reconciliation_scope(alias)
523
+ payload, _ = runner.native_auth_reconciliation_payload(outputs, scope, {})
524
+ microsoft_connection = next(
525
+ item
526
+ for item in payload["connections"]
527
+ if item["connectionKey"] == "microsoft:organizations"
528
+ )
529
+ assert microsoft_connection["issuerUrl"] == (f"https://login.microsoftonline.com/{alias}/v2.0")
530
+
531
+
532
+ @pytest.mark.parametrize("tenant", ["tenant.example", "contoso", "9d65869f", ""])
533
+ def test_native_auth_reconciliation_rejects_arbitrary_tenant_values(tenant: str) -> None:
534
+ runner = load_runner()
535
+ outputs, scope = _microsoft_reconciliation_scope(tenant)
536
+ with pytest.raises(RuntimeError, match="Entra directory GUID or one of"):
537
+ runner.native_auth_reconciliation_payload(outputs, scope, {})
538
+
539
+
229
540
  def tenant_entra_operation(action: str, revision: int = 1) -> dict:
230
541
  tenant_id = "00000000-0000-4000-8000-000000000123"
231
542
  payload = {
@@ -539,6 +850,214 @@ def test_sync_release_artifacts_stages_artifacts_from_platform_bundle(
539
850
  assert {artifact["source"] for artifact in runner.RELEASE_EVIDENCE["artifacts"]} == {"bundle"}
540
851
 
541
852
 
853
+ def test_packaged_agentcore_control_runtime_is_exact_and_runnable(
854
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
855
+ ) -> None:
856
+ runner = load_runner()
857
+ repo_root = Path(__file__).resolve().parents[4]
858
+ release_dir = tmp_path / "release"
859
+ runner_dir = release_dir / "bundles" / "platform" / "runner"
860
+ subprocess.run(
861
+ [
862
+ "bash",
863
+ str(repo_root / "scripts/release/package-agentcore-control-runtime.sh"),
864
+ str(runner_dir),
865
+ ],
866
+ cwd=repo_root,
867
+ check=True,
868
+ text=True,
869
+ )
870
+
871
+ monkeypatch.setattr(runner, "RELEASE", release_dir)
872
+ monkeypatch.setattr(runner, "RELEASE_EVIDENCE", {})
873
+ monkeypatch.delenv("THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR", raising=False)
874
+
875
+ evidence = runner.prepare_agentcore_control_runtime()
876
+
877
+ assert evidence["sdkVersion"] == "3.1089.0"
878
+ assert int(evidence["nodeVersion"].removeprefix("v").split(".", 1)[0]) >= 22
879
+ assert evidence["bundledRuntimeVerified"] is True
880
+ assert evidence["bundledEntrypoints"] == [
881
+ "preflight.js",
882
+ "reconcile_pi_runtime.js",
883
+ "reconcile_twenty_provider.js",
884
+ ]
885
+ assert json.loads(
886
+ (runner_dir / "agentcore-control-runtime" / "package.json").read_text(encoding="utf-8")
887
+ ) == {"type": "module"}
888
+ assert os.environ["THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR"] == str(
889
+ runner_dir / "agentcore-control-runtime"
890
+ )
891
+ assert all(item["sdkImportReady"] for item in evidence["entrypointPreflights"])
892
+
893
+
894
+ @pytest.mark.parametrize("version", ["v22.14.0", "v24.1.0", "26.5.0"])
895
+ def test_agentcore_control_runtime_accepts_supported_node_versions(version: str) -> None:
896
+ runner = load_runner()
897
+
898
+ assert runner.validate_agentcore_node_runtime(version).startswith("v")
899
+
900
+
901
+ @pytest.mark.parametrize("version", ["v20.19.5", "v18.20.8", "unknown", ""])
902
+ def test_agentcore_control_runtime_rejects_unsupported_node_versions(version: str) -> None:
903
+ runner = load_runner()
904
+
905
+ with pytest.raises(RuntimeError, match="Node.js 22|Cannot determine"):
906
+ runner.validate_agentcore_node_runtime(version)
907
+
908
+
909
+ @pytest.mark.parametrize("action", ["deploy", "update", "plan", "apply", "destroy"])
910
+ def test_agentcore_control_runtime_is_required_even_when_proof_is_disabled(
911
+ action: str,
912
+ ) -> None:
913
+ runner = load_runner()
914
+
915
+ assert runner.requires_agentcore_control_runtime(
916
+ action,
917
+ {"enableAgentCoreHarness": False},
918
+ None,
919
+ )
920
+
921
+
922
+ def test_web_only_release_sync_does_not_require_agentcore_control_runtime() -> None:
923
+ runner = load_runner()
924
+
925
+ assert runner.release_sync_request("web", {}, None) == {
926
+ "artifact_types": {"static-site"},
927
+ "artifact_names": {"web"},
928
+ }
929
+ assert not runner.requires_agentcore_control_runtime("web", {}, None)
930
+
931
+
932
+ def _state_document(check_results):
933
+ return {
934
+ "version": 4,
935
+ "terraform_version": "1.9.8",
936
+ "serial": 820,
937
+ "lineage": "61f4093e-41da-ac29-852a-267bc853e24f",
938
+ "resources": [{"type": "aws_s3_bucket", "name": "evidence"}],
939
+ "check_results": check_results,
940
+ }
941
+
942
+
943
+ def test_state_identity_is_stable_across_check_results_render_order(
944
+ monkeypatch: pytest.MonkeyPatch,
945
+ ) -> None:
946
+ runner = load_runner()
947
+ check_a = {"object_kind": "var", "config_addr": "module.a.var.x", "status": "pass"}
948
+ check_b = {"object_kind": "var", "config_addr": "module.b.var.y", "status": "pass"}
949
+ pulls = iter(
950
+ [
951
+ json.dumps(_state_document([check_a, check_b]), indent=2) + "\n",
952
+ json.dumps(_state_document([check_b, check_a]), indent=2) + "\n",
953
+ ]
954
+ )
955
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: next(pulls))
956
+
957
+ first = runner.terraform_state_identity()
958
+ second = runner.terraform_state_identity()
959
+
960
+ assert first == second
961
+ assert first["lineage"] == "61f4093e-41da-ac29-852a-267bc853e24f"
962
+ assert first["serial"] == 820
963
+ assert first["terraformVersion"] == "1.9.8"
964
+
965
+
966
+ def test_state_identity_changes_when_resources_change(
967
+ monkeypatch: pytest.MonkeyPatch,
968
+ ) -> None:
969
+ runner = load_runner()
970
+ base = _state_document([])
971
+ changed = dict(base, serial=821, resources=[])
972
+ pulls = iter([json.dumps(base), json.dumps(changed)])
973
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: next(pulls))
974
+
975
+ first = runner.terraform_state_identity()
976
+ second = runner.terraform_state_identity()
977
+
978
+ assert first["sha256"] != second["sha256"]
979
+ assert second["serial"] == 821
980
+
981
+
982
+ def test_state_identity_still_refuses_missing_lineage(
983
+ monkeypatch: pytest.MonkeyPatch,
984
+ ) -> None:
985
+ runner = load_runner()
986
+ document = _state_document([])
987
+ document.pop("lineage")
988
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: json.dumps(document))
989
+
990
+ with pytest.raises(RuntimeError, match="no lineage"):
991
+ runner.terraform_state_identity()
992
+
993
+
994
+ def test_deployment_status_pointer_writes_for_saved_plan_apply(
995
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
996
+ ) -> None:
997
+ runner = load_runner()
998
+ monkeypatch.chdir(tmp_path)
999
+ monkeypatch.setenv("THINKWORK_EVIDENCE_BUCKET", "evidence-bucket")
1000
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_ACTION", "apply")
1001
+ monkeypatch.delenv("THINKWORK_MANAGED_APP_OPERATION", raising=False)
1002
+ commands = []
1003
+ monkeypatch.setattr(runner, "read_current_status_pointer", lambda _bucket: {})
1004
+ monkeypatch.setattr(runner, "run", lambda args, **_kwargs: commands.append(args))
1005
+
1006
+ runner.write_deployment_status_pointer(
1007
+ "succeeded", {"stage": "tei-e2e", "account_id": "637423202447"}
1008
+ )
1009
+
1010
+ assert any(
1011
+ arg.endswith("deployment/status/current.json") for command in commands for arg in command
1012
+ )
1013
+
1014
+
1015
+ def test_saved_plan_apply_stages_the_approved_release_artifacts() -> None:
1016
+ runner = load_runner()
1017
+
1018
+ assert runner.release_sync_request("apply", {}, None) == {
1019
+ "artifact_types": None,
1020
+ "artifact_names": None,
1021
+ }
1022
+
1023
+
1024
+ def test_packaged_agentcore_control_runtime_rebuild_removes_stale_files(
1025
+ tmp_path: Path,
1026
+ ) -> None:
1027
+ repo_root = Path(__file__).resolve().parents[4]
1028
+ runner_dir = tmp_path / "runner"
1029
+ runtime_dir = runner_dir / "agentcore-control-runtime"
1030
+ runtime_dir.mkdir(parents=True)
1031
+ (runtime_dir / "stale-chunk.js").write_text("stale\n", encoding="utf-8")
1032
+
1033
+ subprocess.run(
1034
+ [
1035
+ "bash",
1036
+ str(repo_root / "scripts/release/package-agentcore-control-runtime.sh"),
1037
+ str(runner_dir),
1038
+ ],
1039
+ cwd=repo_root,
1040
+ check=True,
1041
+ text=True,
1042
+ )
1043
+
1044
+ manifest = json.loads(
1045
+ (runner_dir / "agentcore-control-runtime.json").read_text(encoding="utf-8")
1046
+ )
1047
+ paths = {item["path"] for item in manifest["files"]}
1048
+ generated = {
1049
+ str(path.relative_to(runtime_dir)) for path in runtime_dir.rglob("*") if path.is_file()
1050
+ }
1051
+ assert "stale-chunk.js" not in generated
1052
+ assert paths == generated
1053
+ assert {path for path in paths if "/" not in path} == {
1054
+ "package.json",
1055
+ "preflight.js",
1056
+ "reconcile_pi_runtime.js",
1057
+ "reconcile_twenty_provider.js",
1058
+ }
1059
+
1060
+
542
1061
  def test_ensure_release_manifest_available_accepts_manifest_file_digest(
543
1062
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
544
1063
  ) -> None:
@@ -576,13 +1095,13 @@ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
576
1095
  bundle_path = tmp_path / "platform-artifacts.tar.gz"
577
1096
  lambda_bytes = b"lambda-zip"
578
1097
  web_bytes = b"web-tarball"
579
- docs_bytes = b"docs-tarball"
1098
+ extra_bytes = b"extra-tarball"
580
1099
  write_tar(
581
1100
  bundle_path,
582
1101
  {
583
1102
  "lambdas/graphql-http.zip": lambda_bytes,
584
1103
  "static/web.tar.gz": web_bytes,
585
- "static/docs.tar.gz": docs_bytes,
1104
+ "static/extra.tar.gz": extra_bytes,
586
1105
  },
587
1106
  )
588
1107
  artifacts = [
@@ -605,13 +1124,13 @@ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
605
1124
  "sizeBytes": len(web_bytes),
606
1125
  },
607
1126
  {
608
- "name": "docs",
1127
+ "name": "extra",
609
1128
  "type": "static-site",
610
- "fileName": "docs.tar.gz",
611
- "relativePath": "static/docs.tar.gz",
1129
+ "fileName": "extra.tar.gz",
1130
+ "relativePath": "static/extra.tar.gz",
612
1131
  "url": None,
613
- "sha256": digest(docs_bytes),
614
- "sizeBytes": len(docs_bytes),
1132
+ "sha256": digest(extra_bytes),
1133
+ "sizeBytes": len(extra_bytes),
615
1134
  },
616
1135
  ]
617
1136
  manifest_sha = write_manifest(
@@ -629,10 +1148,12 @@ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
629
1148
  monkeypatch.setenv("THINKWORK_RELEASE_ARTIFACT_BUCKET", "thinkwork-artifacts")
630
1149
  monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
631
1150
 
632
- static_files = runner.sync_release_artifacts(
633
- artifact_types={"static-site"},
634
- artifact_names={"web"},
635
- )
1151
+ release_request = runner.release_sync_request("web", {}, None)
1152
+ assert release_request == {
1153
+ "artifact_types": {"static-site"},
1154
+ "artifact_names": {"web"},
1155
+ }
1156
+ static_files = runner.sync_release_artifacts(**release_request)
636
1157
 
637
1158
  assert static_files == {"web": release_dir / "static/web.tar.gz"}
638
1159
  assert static_files["web"].read_bytes() == web_bytes
@@ -1467,6 +1988,13 @@ def test_write_runner_files_persists_selected_release_to_controller_module(
1467
1988
  assert "deployment_release_manifest_signature_url" in main_tf
1468
1989
  assert "deployment_release_manifest_trusted_keys_json" in main_tf
1469
1990
  assert "deployment_terraform_module_source" in main_tf
1991
+ assert 'required_version = ">= 1.8.5"' in main_tf
1992
+ assert (
1993
+ "from = "
1994
+ "module.thinkwork.module.agentcore_proof_identity."
1995
+ "terraform_data.twenty_identity_lifecycle"
1996
+ ) in main_tf
1997
+ assert "destroy = false" in main_tf
1470
1998
 
1471
1999
 
1472
2000
  def _cognito_email_runner_env(runner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
@@ -1554,17 +2082,11 @@ def test_write_runner_files_threads_cognito_email_vars_from_payload(
1554
2082
  assert f"= var.{name}" in main_tf
1555
2083
 
1556
2084
 
1557
- def test_write_runner_files_repins_stale_thinkwork_git_module_source_to_release(
2085
+ def test_write_runner_files_threads_agentcore_harness_config_from_preserved_payload(
1558
2086
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1559
2087
  ) -> None:
1560
2088
  runner = load_runner()
1561
2089
  tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1562
- monkeypatch.setenv(
1563
- "THINKWORK_TERRAFORM_MODULE_SOURCE",
1564
- "git::https://github.com/thinkwork-ai/thinkwork.git"
1565
- "//terraform/modules/thinkwork?ref=c66a3aa7f3a5606c66b920b40b39af57a7cc06d0",
1566
- )
1567
- monkeypatch.setenv("THINKWORK_TERRAFORM_MODULE_VERSION", "0.1.0-canary.150")
1568
2090
 
1569
2091
  vars_json = runner.write_runner_files(
1570
2092
  {
@@ -1573,25 +2095,69 @@ def test_write_runner_files_repins_stale_thinkwork_git_module_source_to_release(
1573
2095
  "awsAccountId": "637423202447",
1574
2096
  "dbPassword": "db-secret",
1575
2097
  "apiAuthSecret": "api-secret",
2098
+ "preservedConfig": {
2099
+ "enableAgentCoreHarness": True,
2100
+ "agentCoreHarnessTenantSlug": "tei",
2101
+ "agentCoreHarnessOwnerAllowlist": "owner-a,owner-b",
2102
+ },
1576
2103
  },
1577
2104
  {},
1578
2105
  )
1579
2106
 
1580
- main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
2107
+ assert vars_json["enable_agentcore_multiplayer_proof"] is True
2108
+ assert vars_json["agentcore_multiplayer_proof_tenant_slug"] == "tei"
2109
+ assert vars_json["agentcore_multiplayer_proof_owner_allowlist"] == "owner-a,owner-b"
1581
2110
 
1582
- assert "ref=abc123" in main_tf
1583
- assert "ref=c66a3aa7f3a5606c66b920b40b39af57a7cc06d0" not in main_tf
1584
- assert vars_json["deployment_terraform_module_source"].endswith(
1585
- "//terraform/modules/thinkwork?ref=abc123"
1586
- )
1587
- assert vars_json["deployment_terraform_module_version"] == ""
2111
+ tfvars = json.loads((tf_dir / "terraform.auto.tfvars.json").read_text(encoding="utf-8"))
2112
+ assert tfvars["enable_agentcore_multiplayer_proof"] is True
2113
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
2114
+ for name in (
2115
+ "enable_agentcore_multiplayer_proof",
2116
+ "agentcore_multiplayer_proof_tenant_slug",
2117
+ "agentcore_multiplayer_proof_owner_allowlist",
2118
+ ):
2119
+ assert f'variable "{name}"' in main_tf
2120
+ assert f"= var.{name}" in main_tf
1588
2121
 
1589
2122
 
1590
- def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default_empty(
2123
+ def test_write_runner_files_repins_stale_thinkwork_git_module_source_to_release(
1591
2124
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1592
2125
  ) -> None:
1593
2126
  runner = load_runner()
1594
- _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2127
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2128
+ monkeypatch.setenv(
2129
+ "THINKWORK_TERRAFORM_MODULE_SOURCE",
2130
+ "git::https://github.com/thinkwork-ai/thinkwork.git"
2131
+ "//terraform/modules/thinkwork?ref=c66a3aa7f3a5606c66b920b40b39af57a7cc06d0",
2132
+ )
2133
+ monkeypatch.setenv("THINKWORK_TERRAFORM_MODULE_VERSION", "0.1.0-canary.150")
2134
+
2135
+ vars_json = runner.write_runner_files(
2136
+ {
2137
+ "stage": "tei-e2e",
2138
+ "awsRegion": "us-east-1",
2139
+ "awsAccountId": "637423202447",
2140
+ "dbPassword": "db-secret",
2141
+ "apiAuthSecret": "api-secret",
2142
+ },
2143
+ {},
2144
+ )
2145
+
2146
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
2147
+
2148
+ assert "ref=abc123" in main_tf
2149
+ assert "ref=c66a3aa7f3a5606c66b920b40b39af57a7cc06d0" not in main_tf
2150
+ assert vars_json["deployment_terraform_module_source"].endswith(
2151
+ "//terraform/modules/thinkwork?ref=abc123"
2152
+ )
2153
+ assert vars_json["deployment_terraform_module_version"] == ""
2154
+
2155
+
2156
+ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default_empty(
2157
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2158
+ ) -> None:
2159
+ runner = load_runner()
2160
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1595
2161
  secret_arn = "arn:aws:ses:us-east-1:637423202447:identity/secret.example.com"
1596
2162
 
1597
2163
  vars_json = runner.write_runner_files(
@@ -1649,6 +2215,54 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1649
2215
  assert vars_json_retired["finalize_auth_retirement"] is True
1650
2216
 
1651
2217
 
2218
+ def test_write_runner_files_threads_additional_admin_callback_urls(
2219
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2220
+ ) -> None:
2221
+ runner = load_runner()
2222
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2223
+
2224
+ # runner-secrets carries the customer's hosted console origins; a
2225
+ # comma-separated string and a JSON-array string must both parse, and the
2226
+ # module wiring must be present so the URLs actually reach the web clients.
2227
+ vars_json = runner.write_runner_files(
2228
+ {
2229
+ "stage": "mcpherson",
2230
+ "awsRegion": "us-east-1",
2231
+ "awsAccountId": "024350822488",
2232
+ "dbPassword": "db-secret",
2233
+ "apiAuthSecret": "api-secret",
2234
+ },
2235
+ {
2236
+ "adminCallbackUrls": (
2237
+ "https://console.example.net, https://console.example.net/auth/callback"
2238
+ ),
2239
+ "adminLogoutUrls": '["https://console.example.net"]',
2240
+ },
2241
+ )
2242
+ assert vars_json["additional_admin_callback_urls"] == [
2243
+ "https://console.example.net",
2244
+ "https://console.example.net/auth/callback",
2245
+ ]
2246
+ assert vars_json["additional_admin_logout_urls"] == ["https://console.example.net"]
2247
+ main_tf = (runner.TF / "main.tf").read_text(encoding="utf-8")
2248
+ assert "additional_admin_callback_urls = var.additional_admin_callback_urls" in main_tf
2249
+ assert "additional_admin_logout_urls = var.additional_admin_logout_urls" in main_tf
2250
+
2251
+ # Default: no runner-secrets keys -> empty lists (no behavior change).
2252
+ vars_json_default = runner.write_runner_files(
2253
+ {
2254
+ "stage": "mcpherson",
2255
+ "awsRegion": "us-east-1",
2256
+ "awsAccountId": "024350822488",
2257
+ "dbPassword": "db-secret",
2258
+ "apiAuthSecret": "api-secret",
2259
+ },
2260
+ {},
2261
+ )
2262
+ assert vars_json_default["additional_admin_callback_urls"] == []
2263
+ assert vars_json_default["additional_admin_logout_urls"] == []
2264
+
2265
+
1652
2266
  def test_write_runner_files_defaults_existing_preauth_stack_to_coexistence(
1653
2267
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1654
2268
  ) -> None:
@@ -1733,49 +2347,13 @@ def test_main_reconciles_native_auth_before_destructive_schema() -> None:
1733
2347
  source = Path(__file__).with_name("runner.py").read_text(encoding="utf-8")
1734
2348
  post_apply = source[source.index("def main():") :]
1735
2349
  reconciliation = post_apply.index(
1736
- "CONTROLLER_EVIDENCE[\"authReconciliation\"] = reconcile_native_auth_metadata"
2350
+ 'CONTROLLER_EVIDENCE["authReconciliation"] = reconcile_native_auth_metadata'
1737
2351
  )
1738
2352
  schema = post_apply.index("push_database_schema(outputs_path, vars_json)", reconciliation)
1739
2353
 
1740
2354
  assert reconciliation < schema
1741
2355
 
1742
2356
 
1743
- def test_write_runner_files_wires_analyst_lambda_vpc_egress(
1744
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1745
- ) -> None:
1746
- """analyst_lambda_vpc_egress needs all three wiring points (vars_json,
1747
- root variable, module argument) and reads runner-secrets first — the
1748
- durable per-environment home for the flag — with a payload fallback.
1749
- Secrets Manager JSON strings ("true") must land as real booleans."""
1750
- runner = load_runner()
1751
- tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1752
- base_payload = {
1753
- "stage": "tei-e2e",
1754
- "awsRegion": "us-east-1",
1755
- "awsAccountId": "637423202447",
1756
- "dbPassword": "db-secret",
1757
- "apiAuthSecret": "api-secret",
1758
- }
1759
-
1760
- assert runner.write_runner_files(base_payload, {})["analyst_lambda_vpc_egress"] is False
1761
- assert (
1762
- runner.write_runner_files(base_payload, {"analystLambdaVpcEgress": "true"})[
1763
- "analyst_lambda_vpc_egress"
1764
- ]
1765
- is True
1766
- )
1767
- assert (
1768
- runner.write_runner_files({**base_payload, "analystLambdaVpcEgress": True}, {})[
1769
- "analyst_lambda_vpc_egress"
1770
- ]
1771
- is True
1772
- )
1773
-
1774
- main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
1775
- assert 'variable "analyst_lambda_vpc_egress"' in main_tf
1776
- assert "= var.analyst_lambda_vpc_egress" in main_tf
1777
-
1778
-
1779
2357
  def test_write_runner_files_threads_customer_domain_vars_from_payload(
1780
2358
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1781
2359
  ) -> None:
@@ -3492,140 +4070,877 @@ def test_write_controller_release_selection_to_ssm_skips_empty_git_module_versio
3492
4070
  assert "/thinkwork/dev/deployment/terraform-module-version" not in written_names
3493
4071
 
3494
4072
 
3495
- def test_terraform_plan_summary_counts_resource_actions() -> None:
4073
+ def _approval_payload(action: str, *, plan: bool, apply: bool) -> dict:
4074
+ return {
4075
+ "contract": "thinkwork.deployment.controller.v1",
4076
+ "action": action,
4077
+ "phase": action,
4078
+ "sessionId": f"session-{action}",
4079
+ "environmentName": "tei-e2e",
4080
+ "awsAccountId": "637423202447",
4081
+ "awsRegion": "us-east-1",
4082
+ "release": {
4083
+ "version": "v0.1.0-canary.381",
4084
+ "manifestUrl": "https://example.test/thinkwork-release.json",
4085
+ "manifestSha256": "a" * 64,
4086
+ },
4087
+ "agentcorePiSourceImageUri": (
4088
+ "637423202447.dkr.ecr.us-east-1.amazonaws.com/"
4089
+ "thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
4090
+ ),
4091
+ "preservedConfig": {
4092
+ "enableAgentCoreHarness": True,
4093
+ "agentCoreHarnessTenantSlug": "tei",
4094
+ },
4095
+ "operation": {
4096
+ "kind": "foundation",
4097
+ "action": action,
4098
+ "targetAction": "update",
4099
+ "plan": plan,
4100
+ "apply": apply,
4101
+ "destroy": False,
4102
+ },
4103
+ }
4104
+
4105
+
4106
+ def test_plan_phase_never_implies_apply() -> None:
3496
4107
  runner = load_runner()
4108
+ payload = _approval_payload("plan", plan=True, apply=False)
3497
4109
 
3498
- summary = runner.terraform_plan_summary(
3499
- {
3500
- "format_version": "1.2",
3501
- "terraform_version": "1.8.0",
3502
- "resource_changes": [
3503
- {"change": {"actions": ["create"]}},
3504
- {"change": {"actions": ["update"]}},
3505
- {"change": {"actions": ["delete", "create"]}},
3506
- {"change": {"actions": ["create"]}},
3507
- ],
3508
- }
3509
- )
4110
+ assert runner.validate_terraform_execution_phase(payload, "plan") == "plan"
3510
4111
 
3511
- assert summary == {
3512
- "formatVersion": "1.2",
3513
- "terraformVersion": "1.8.0",
3514
- "resourceChangeCount": 4,
3515
- "resourceChangesByAction": {
3516
- "create": 2,
3517
- "update": 1,
3518
- "delete,create": 1,
3519
- },
4112
+
4113
+ def test_existing_managed_application_plan_remains_plan_only() -> None:
4114
+ runner = load_runner()
4115
+ payload = {
4116
+ "action": "plan",
4117
+ "phase": "plan",
4118
+ "appKey": "twenty",
4119
+ "operation": "ENABLE",
3520
4120
  }
3521
4121
 
4122
+ assert runner.validate_terraform_execution_phase(payload, "plan") == "plan"
3522
4123
 
3523
- def test_self_update_runner_script_copies_release_runner_to_s3(
3524
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4124
+
4125
+ def test_explicit_plan_execution_never_invokes_terraform_apply(
4126
+ monkeypatch: pytest.MonkeyPatch,
3525
4127
  ) -> None:
3526
4128
  runner = load_runner()
3527
- source_dir = tmp_path / "source"
3528
- script = source_dir / "terraform/modules/app/deployment-control-plane/runner.py"
3529
- script.parent.mkdir(parents=True)
3530
- script.write_text("# release runner\n", encoding="utf-8")
3531
-
3532
- monkeypatch.setattr(runner, "SOURCE", source_dir)
3533
- monkeypatch.setenv(
3534
- "THINKWORK_RUNNER_SCRIPT_S3_URI",
3535
- "s3://evidence-bucket/runner/thinkwork-runner.py",
3536
- )
3537
4129
  calls: list[list[str]] = []
3538
- monkeypatch.setattr(runner, "run", lambda args, **_kw: calls.append(args))
4130
+ monkeypatch.setattr(
4131
+ runner.subprocess,
4132
+ "run",
4133
+ lambda args, **_kwargs: calls.append(args) or SimpleNamespace(returncode=0),
4134
+ )
4135
+ monkeypatch.setattr(
4136
+ runner,
4137
+ "write_terraform_plan_evidence",
4138
+ lambda *_args, **_kwargs: {"status": "awaiting-approval"},
4139
+ )
3539
4140
 
3540
- runner.self_update_runner_script()
4141
+ result = runner.execute_terraform_plan_phase(
4142
+ _approval_payload("plan", plan=True, apply=False),
4143
+ {"lineage": "lineage", "serial": 1},
4144
+ "update",
4145
+ "plan",
4146
+ [],
4147
+ )
3541
4148
 
3542
- assert calls == [
3543
- [
3544
- "aws",
3545
- "s3",
3546
- "cp",
3547
- str(script),
3548
- "s3://evidence-bucket/runner/thinkwork-runner.py",
3549
- ]
3550
- ]
4149
+ assert result.returncode == 0
4150
+ assert calls == [["terraform", "plan", "-out=tfplan", "-no-color"]]
4151
+ assert runner.TERRAFORM_EVIDENCE["plan"]["status"] == "awaiting-approval"
3551
4152
 
3552
4153
 
3553
- def test_self_update_runner_script_skips_when_source_missing(
3554
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4154
+ def test_legacy_execution_still_applies_its_successful_plan(
4155
+ monkeypatch: pytest.MonkeyPatch,
3555
4156
  ) -> None:
3556
4157
  runner = load_runner()
3557
- monkeypatch.setattr(runner, "SOURCE", tmp_path / "missing-source")
3558
- monkeypatch.setenv(
3559
- "THINKWORK_RUNNER_SCRIPT_S3_URI",
3560
- "s3://evidence-bucket/runner/thinkwork-runner.py",
3561
- )
3562
4158
  calls: list[list[str]] = []
3563
- monkeypatch.setattr(runner, "run", lambda args, **_kw: calls.append(args))
4159
+ monkeypatch.setattr(
4160
+ runner.subprocess,
4161
+ "run",
4162
+ lambda args, **_kwargs: calls.append(args) or SimpleNamespace(returncode=0),
4163
+ )
4164
+ monkeypatch.setattr(
4165
+ runner,
4166
+ "write_terraform_plan_evidence",
4167
+ lambda *_args, **_kwargs: {"status": "awaiting-approval"},
4168
+ )
3564
4169
 
3565
- runner.self_update_runner_script()
4170
+ runner.execute_terraform_plan_phase(
4171
+ _approval_payload("update", plan=True, apply=True),
4172
+ {"lineage": "lineage", "serial": 1},
4173
+ "update",
4174
+ "legacy",
4175
+ [],
4176
+ )
3566
4177
 
3567
- assert calls == []
4178
+ assert calls == [
4179
+ ["terraform", "plan", "-out=tfplan", "-no-color"],
4180
+ ["terraform", "apply", "-auto-approve", "-no-color", "tfplan"],
4181
+ ]
3568
4182
 
3569
4183
 
3570
- # ---------------------------------------------------------------------------
3571
- # First-admin bootstrap (first-run provisioning)
3572
- # ---------------------------------------------------------------------------
4184
+ @pytest.mark.parametrize("action", ["deploy", "update", "destroy"])
4185
+ def test_legacy_combined_terraform_actions_remain_compatible(action: str) -> None:
4186
+ runner = load_runner()
4187
+ payload = _approval_payload(action, plan=True, apply=True)
3573
4188
 
4189
+ assert runner.validate_terraform_execution_phase(payload, action) == "legacy"
3574
4190
 
3575
- def test_first_admin_tenant_slug_prefers_customer_domain_label() -> None:
4191
+
4192
+ def test_legacy_and_approved_apply_reconcile_native_auth_schema() -> None:
3576
4193
  runner = load_runner()
3577
- slug = runner.first_admin_tenant_slug(
3578
- {}, {}, {"stage": "acme-prod", "customer_domain": "acme.thinkwork.ai"}
4194
+
4195
+ assert runner.should_reconcile_native_auth_schema("legacy", "deploy", {}) is True
4196
+ assert runner.should_reconcile_native_auth_schema("legacy", "update", {}) is True
4197
+ assert runner.should_reconcile_native_auth_schema("apply", "update", {}) is True
4198
+ assert runner.should_reconcile_native_auth_schema("plan", "update", {}) is False
4199
+ assert runner.should_reconcile_native_auth_schema("legacy", "destroy", {}) is False
4200
+ assert (
4201
+ runner.should_reconcile_native_auth_schema(
4202
+ "legacy",
4203
+ "update",
4204
+ {"appKey": "twenty"},
4205
+ )
4206
+ is False
3579
4207
  )
3580
- assert slug == "acme"
3581
4208
 
3582
4209
 
3583
- def test_first_admin_tenant_slug_falls_back_to_stage() -> None:
4210
+ def test_apply_phase_requires_separate_approved_plan() -> None:
3584
4211
  runner = load_runner()
3585
- slug = runner.first_admin_tenant_slug({}, {}, {"stage": "acme", "customer_domain": ""})
3586
- assert slug == "acme"
4212
+ payload = _approval_payload("apply", plan=False, apply=True)
3587
4213
 
4214
+ with pytest.raises(RuntimeError, match="requires approvedPlan"):
4215
+ runner.validate_terraform_execution_phase(payload, "apply")
3588
4216
 
3589
- def test_first_admin_tenant_slug_rejects_domain_mismatch() -> None:
3590
- runner = load_runner()
3591
- with pytest.raises(RuntimeError, match="KTD8"):
3592
- runner.first_admin_tenant_slug(
3593
- {},
3594
- {"tenantSlug": "other"},
3595
- {"stage": "acme", "customer_domain": "acme.thinkwork.ai"},
3596
- )
4217
+ payload["approvedPlan"] = {
4218
+ "descriptor": {"s3Uri": "s3://evidence/descriptor.json", "sha256": "a" * 64}
4219
+ }
4220
+ assert runner.validate_terraform_execution_phase(payload, "apply") == "apply"
3597
4221
 
3598
4222
 
3599
- def test_first_admin_tenant_slug_rejects_invalid_slug() -> None:
4223
+ def test_plan_and_apply_share_only_immutable_deployment_config_digest() -> None:
3600
4224
  runner = load_runner()
3601
- with pytest.raises(RuntimeError, match="slug pattern"):
3602
- runner.first_admin_tenant_slug({}, {}, {"stage": "Bad_Stage!", "customer_domain": ""})
4225
+ planned = _approval_payload("plan", plan=True, apply=False)
4226
+ applied = _approval_payload("apply", plan=False, apply=True)
4227
+ applied["approvedPlan"] = {
4228
+ "descriptor": {"s3Uri": "s3://evidence/descriptor.json", "sha256": "a" * 64}
4229
+ }
3603
4230
 
4231
+ assert runner.controller_input_sha256(planned) != runner.controller_input_sha256(applied)
4232
+ assert runner.deployment_config_sha256(planned) == runner.deployment_config_sha256(applied)
3604
4233
 
3605
- def test_first_admin_email_takes_first_valid_entry() -> None:
4234
+ applied["preservedConfig"]["agentCoreHarnessTenantSlug"] = "wrong"
4235
+ assert runner.deployment_config_sha256(planned) != runner.deployment_config_sha256(applied)
4236
+
4237
+
4238
+ def test_saved_plan_apply_revalidates_config_release_state_vars_and_rendered_plan(
4239
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4240
+ ) -> None:
3606
4241
  runner = load_runner()
3607
- assert (
3608
- runner.first_admin_email({"platform_operator_emails": "ops@acme.com, two@acme.com"})
3609
- == "ops@acme.com"
4242
+ work = tmp_path / "work"
4243
+ tf = work / "terraform"
4244
+ tf.mkdir(parents=True)
4245
+ monkeypatch.setattr(runner, "WORK", work)
4246
+ monkeypatch.setattr(runner, "TF", tf)
4247
+ payload = _approval_payload("apply", plan=False, apply=True)
4248
+ payload["approvedPlan"] = {
4249
+ "descriptor": {"s3Uri": "s3://evidence/descriptor.json", "sha256": "d" * 64}
4250
+ }
4251
+ state = {
4252
+ "lineage": "lineage",
4253
+ "serial": 7,
4254
+ "sha256": "s" * 64,
4255
+ "terraformVersion": "1.8.5",
4256
+ }
4257
+ tfvars = b'{"stage":"tei-e2e"}\n'
4258
+ (tf / "terraform.auto.tfvars.json").write_bytes(tfvars)
4259
+ saved_plan_bytes = b"opaque-saved-plan"
4260
+ accepted_plan = json.dumps(
4261
+ {
4262
+ "resource_changes": [],
4263
+ "format_version": "1.2",
4264
+ "timestamp": "2026-07-20T18:40:21Z",
4265
+ "configuration": {
4266
+ "expression": {
4267
+ "references": [
4268
+ "aws_security_group.twenty.id",
4269
+ "aws_security_group.twenty",
4270
+ "var.cache_port",
4271
+ ]
4272
+ }
4273
+ },
4274
+ }
4275
+ ).encode()
4276
+ rendered_plan = json.dumps(
4277
+ {
4278
+ "format_version": "1.2",
4279
+ "resource_changes": [],
4280
+ "timestamp": "2026-07-20T18:49:12Z",
4281
+ "configuration": {
4282
+ "expression": {
4283
+ "references": [
4284
+ "var.cache_port",
4285
+ "aws_security_group.twenty.id",
4286
+ "aws_security_group.twenty",
4287
+ ]
4288
+ }
4289
+ },
4290
+ },
4291
+ indent=2,
4292
+ ).encode()
4293
+ descriptor = {
4294
+ "contract": runner.TERRAFORM_PLAN_APPROVAL_CONTRACT,
4295
+ "deploymentConfigSha256": runner.deployment_config_sha256(payload),
4296
+ "release": runner.release_selection(payload),
4297
+ "state": state,
4298
+ "terraformVariablesSha256": digest(tfvars),
4299
+ "savedPlan": {
4300
+ "s3Uri": "s3://evidence/plan.bin",
4301
+ "sha256": digest(saved_plan_bytes),
4302
+ },
4303
+ "jsonPlan": {
4304
+ "s3Uri": "s3://evidence/plan.json",
4305
+ "sha256": digest(accepted_plan),
4306
+ },
4307
+ "summary": {"resourceChangeCount": 0},
4308
+ "plannedAction": "update",
4309
+ "sessionId": "plan-session",
4310
+ }
4311
+ monkeypatch.setattr(
4312
+ runner,
4313
+ "load_approved_plan_descriptor",
4314
+ lambda _payload: (descriptor, payload["approvedPlan"]["descriptor"]),
3610
4315
  )
3611
- assert runner.first_admin_email({"platform_operator_emails": ""}) == ""
3612
- assert runner.first_admin_email({"platform_operator_emails": "not-an-email"}) == ""
3613
- assert runner.first_admin_email({"platform_operator_emails": "a'b@acme.com"}) == ""
3614
4316
 
4317
+ def fake_download(uri, destination, expected_sha, _label):
4318
+ if uri == "s3://evidence/plan.bin":
4319
+ content = saved_plan_bytes
4320
+ else:
4321
+ assert uri == "s3://evidence/plan.json"
4322
+ content = accepted_plan
4323
+ destination.write_bytes(content)
4324
+ assert digest(content) == expected_sha
4325
+ return expected_sha
3615
4326
 
3616
- def test_ensure_first_admin_skips_without_admin_email(tmp_path: Path) -> None:
4327
+ def fake_run(args, **kwargs):
4328
+ assert args == ["terraform", "show", "-json", "tfplan"]
4329
+ kwargs["stdout"].write(rendered_plan.decode())
4330
+
4331
+ monkeypatch.setattr(runner, "_download_approved_artifact", fake_download)
4332
+ monkeypatch.setattr(runner, "run", fake_run)
4333
+
4334
+ approved = runner.validate_and_materialize_approved_plan(payload, state)
4335
+ assert approved["plannedAction"] == "update"
4336
+ assert approved["sourceSessionId"] == "plan-session"
4337
+ assert (tf / "tfplan").read_bytes() == saved_plan_bytes
4338
+
4339
+ rendered_plan = b'{"format_version":"1.2","resource_changes":[{"address":"changed"}]}\n'
4340
+ with pytest.raises(RuntimeError, match="no longer renders"):
4341
+ runner.validate_and_materialize_approved_plan(payload, state)
4342
+
4343
+ descriptor["state"] = {**state, "serial": 8}
4344
+ with pytest.raises(RuntimeError, match="state changed after plan approval"):
4345
+ runner.validate_and_materialize_approved_plan(payload, state)
4346
+
4347
+
4348
+ def test_saved_plan_artifacts_must_use_configured_evidence_bucket(
4349
+ monkeypatch: pytest.MonkeyPatch,
4350
+ ) -> None:
3617
4351
  runner = load_runner()
3618
- outputs = tmp_path / "outputs.json"
3619
- outputs.write_text("{}", encoding="utf-8")
3620
- runner.ensure_first_admin(outputs, {"platform_operator_emails": "", "stage": "acme"}, {}, {})
3621
- assert runner.FIRST_ADMIN_EVIDENCE["status"] == "skipped"
4352
+ monkeypatch.setenv("THINKWORK_EVIDENCE_BUCKET", "expected-evidence")
4353
+
4354
+ assert (
4355
+ runner._validated_evidence_s3_uri(
4356
+ "s3://expected-evidence/sessions/plan.json", "approved plan"
4357
+ )
4358
+ == "s3://expected-evidence/sessions/plan.json"
4359
+ )
4360
+ with pytest.raises(RuntimeError, match="configured deployment evidence bucket"):
4361
+ runner._validated_evidence_s3_uri("s3://attacker-controlled/plan.json", "approved plan")
3622
4362
 
3623
4363
 
3624
- def test_ensure_first_admin_skips_established_env_without_slug(
4364
+ def test_tei_v380_saved_plan_apply_refuses_any_delete_action(
3625
4365
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
3626
4366
  ) -> None:
3627
4367
  runner = load_runner()
3628
- outputs = tmp_path / "outputs.json"
4368
+ work = tmp_path / "work"
4369
+ tf = work / "terraform"
4370
+ tf.mkdir(parents=True)
4371
+ monkeypatch.setattr(runner, "WORK", work)
4372
+ monkeypatch.setattr(runner, "TF", tf)
4373
+ payload = _approval_payload("apply", plan=False, apply=True)
4374
+ payload["incidentRecovery"] = {"contract": runner.TEI_V380_RECOVERY_CONTRACT}
4375
+ payload["approvedPlan"] = {
4376
+ "descriptor": {"s3Uri": "s3://evidence/descriptor.json", "sha256": "d" * 64}
4377
+ }
4378
+ state = {
4379
+ "lineage": "lineage",
4380
+ "serial": 7,
4381
+ "sha256": "e" * 64,
4382
+ "terraformVersion": "1.8.5",
4383
+ }
4384
+ tfvars = b'{"stage":"tei-e2e"}\n'
4385
+ (tf / "terraform.auto.tfvars.json").write_bytes(tfvars)
4386
+ saved_plan = b"opaque-saved-plan"
4387
+ rendered_plan = json.dumps(
4388
+ {
4389
+ "format_version": "1.2",
4390
+ "resource_changes": [
4391
+ {
4392
+ "address": "module.thinkwork.proof_resource",
4393
+ "change": {"actions": ["delete"]},
4394
+ }
4395
+ ],
4396
+ }
4397
+ ).encode()
4398
+ descriptor = {
4399
+ "contract": runner.TERRAFORM_PLAN_APPROVAL_CONTRACT,
4400
+ "deploymentConfigSha256": runner.deployment_config_sha256(payload),
4401
+ "release": runner.release_selection(payload),
4402
+ "state": state,
4403
+ "terraformVariablesSha256": digest(tfvars),
4404
+ "savedPlan": {
4405
+ "s3Uri": "s3://evidence/plan.bin",
4406
+ "sha256": digest(saved_plan),
4407
+ },
4408
+ "jsonPlan": {
4409
+ "s3Uri": "s3://evidence/plan.json",
4410
+ "sha256": digest(rendered_plan),
4411
+ },
4412
+ "plannedAction": "update",
4413
+ }
4414
+ monkeypatch.setattr(
4415
+ runner,
4416
+ "load_approved_plan_descriptor",
4417
+ lambda _payload: (descriptor, payload["approvedPlan"]["descriptor"]),
4418
+ )
4419
+
4420
+ def fake_download(uri, destination, _sha, _label):
4421
+ destination.write_bytes(saved_plan if uri.endswith("plan.bin") else rendered_plan)
4422
+
4423
+ monkeypatch.setattr(runner, "_download_approved_artifact", fake_download)
4424
+ monkeypatch.setattr(
4425
+ runner,
4426
+ "run",
4427
+ lambda _args, **kwargs: kwargs["stdout"].write(rendered_plan.decode()),
4428
+ )
4429
+
4430
+ with pytest.raises(RuntimeError, match="contains delete actions"):
4431
+ runner.validate_and_materialize_approved_plan(payload, state)
4432
+
4433
+
4434
+ def _tei_v380_recovery_payload(runner) -> dict:
4435
+ return {
4436
+ "action": "recover",
4437
+ "environmentName": "tei-e2e",
4438
+ "stage": "tei-e2e",
4439
+ "awsAccountId": "637423202447",
4440
+ "accountId": "637423202447",
4441
+ "awsRegion": "us-east-1",
4442
+ "region": "us-east-1",
4443
+ "operation": {
4444
+ "kind": "incident_recovery",
4445
+ **runner.tei_v380_recovery_scope(),
4446
+ },
4447
+ }
4448
+
4449
+
4450
+ def _tei_v380_state(runner) -> dict:
4451
+ return {
4452
+ "bucket": runner.TEI_V380_STATE_BUCKET,
4453
+ "key": runner.TEI_V380_STATE_KEY,
4454
+ "etag": "state-etag",
4455
+ "size": 123,
4456
+ "lastModified": "2026-07-20T13:31:15Z",
4457
+ "serverSideEncryption": "AES256",
4458
+ "versionId": None,
4459
+ "sha256": runner.TEI_V380_STATE_SHA256,
4460
+ "lineage": runner.TEI_V380_STATE_LINEAGE,
4461
+ "serial": runner.TEI_V380_STATE_SERIAL,
4462
+ "terraformVersion": "1.8.5",
4463
+ }
4464
+
4465
+
4466
+ def test_tei_v380_recovery_contract_is_exact_and_rejects_alias_drift() -> None:
4467
+ runner = load_runner()
4468
+ payload = _tei_v380_recovery_payload(runner)
4469
+
4470
+ assert runner.validate_tei_v380_recovery_contract(payload) == (runner.tei_v380_recovery_scope())
4471
+
4472
+ payload["stage"] = "dev"
4473
+ with pytest.raises(RuntimeError, match="stage does not match exact incident scope"):
4474
+ runner.validate_tei_v380_recovery_contract(payload)
4475
+
4476
+ payload = _tei_v380_recovery_payload(runner)
4477
+ payload["operation"]["unexpected"] = True
4478
+ with pytest.raises(RuntimeError, match="unexpected fields"):
4479
+ runner.validate_tei_v380_recovery_contract(payload)
4480
+
4481
+
4482
+ def test_incident_recovery_exclusivity_ignores_only_current_operation(
4483
+ monkeypatch: pytest.MonkeyPatch,
4484
+ ) -> None:
4485
+ runner = load_runner()
4486
+ execution_arn = "arn:aws:states:us-east-1:637423202447:execution:deploy:current"
4487
+ build_id = "runner:current"
4488
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_EXECUTION_ARN", execution_arn)
4489
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_STATE_MACHINE_ARN", "arn:state-machine")
4490
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_RUNNER_PROJECT_NAME", "runner")
4491
+ monkeypatch.setenv("CODEBUILD_BUILD_ID", build_id)
4492
+
4493
+ include_other = False
4494
+
4495
+ def fake_output(args, **_kwargs):
4496
+ if args[1:3] == ["stepfunctions", "list-executions"]:
4497
+ executions = [{"executionArn": execution_arn}]
4498
+ if include_other:
4499
+ executions.append({"executionArn": "arn:other"})
4500
+ return json.dumps({"executions": executions})
4501
+ if args[1:3] == ["codebuild", "list-builds-for-project"]:
4502
+ assert "--no-paginate" in args
4503
+ return json.dumps({"ids": [build_id]})
4504
+ if args[1:3] == ["codebuild", "batch-get-builds"]:
4505
+ return json.dumps({"builds": [{"id": build_id, "buildStatus": "IN_PROGRESS"}]})
4506
+ raise AssertionError(args)
4507
+
4508
+ monkeypatch.setattr(runner, "output", fake_output)
4509
+ assert runner.assert_incident_recovery_operation_is_exclusive() == {
4510
+ "currentExecutionArn": execution_arn,
4511
+ "currentBuildId": build_id,
4512
+ "otherRunningExecutions": 0,
4513
+ "otherRunningBuilds": 0,
4514
+ }
4515
+
4516
+ include_other = True
4517
+ with pytest.raises(RuntimeError, match="Another deployment execution"):
4518
+ runner.assert_incident_recovery_operation_is_exclusive()
4519
+
4520
+
4521
+ def test_incident_recovery_exclusivity_supports_one_managed_transition_execution(
4522
+ monkeypatch: pytest.MonkeyPatch,
4523
+ ) -> None:
4524
+ runner = load_runner()
4525
+ execution_arn = "arn:aws:states:us-east-1:637423202447:execution:deploy:transition"
4526
+ build_id = "runner:transition"
4527
+ monkeypatch.delenv("THINKWORK_DEPLOYMENT_EXECUTION_ARN", raising=False)
4528
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_STATE_MACHINE_ARN", "arn:state-machine")
4529
+ monkeypatch.setenv("THINKWORK_DEPLOYMENT_RUNNER_PROJECT_NAME", "runner")
4530
+ monkeypatch.setenv("CODEBUILD_BUILD_ID", build_id)
4531
+
4532
+ def fake_output(args, **_kwargs):
4533
+ if args[1:3] == ["stepfunctions", "list-executions"]:
4534
+ return json.dumps({"executions": [{"executionArn": execution_arn}]})
4535
+ if args[1:3] == ["codebuild", "list-builds-for-project"]:
4536
+ assert "--no-paginate" in args
4537
+ return json.dumps({"ids": [build_id]})
4538
+ if args[1:3] == ["codebuild", "batch-get-builds"]:
4539
+ return json.dumps({"builds": [{"id": build_id, "buildStatus": "IN_PROGRESS"}]})
4540
+ raise AssertionError(args)
4541
+
4542
+ monkeypatch.setattr(runner, "output", fake_output)
4543
+ assert (
4544
+ runner.assert_incident_recovery_operation_is_exclusive()["currentExecutionArn"]
4545
+ == execution_arn
4546
+ )
4547
+
4548
+
4549
+ def test_tei_v380_recovery_restores_only_exact_resources_without_secret_read(
4550
+ monkeypatch: pytest.MonkeyPatch,
4551
+ ) -> None:
4552
+ runner = load_runner()
4553
+ payload = _tei_v380_recovery_payload(runner)
4554
+ state = _tei_v380_state(runner)
4555
+ calls: list[list[str]] = []
4556
+ evidence: list[tuple[str, dict]] = []
4557
+ secret_describes = 0
4558
+ kms_describes = 0
4559
+
4560
+ monkeypatch.setattr(
4561
+ runner,
4562
+ "assert_incident_recovery_operation_is_exclusive",
4563
+ lambda: {
4564
+ "currentExecutionArn": "arn:current",
4565
+ "currentBuildId": "build:current",
4566
+ "otherRunningExecutions": 0,
4567
+ "otherRunningBuilds": 0,
4568
+ },
4569
+ )
4570
+ monkeypatch.setattr(runner, "terraform_state_object_snapshot", lambda _stage: state)
4571
+ monkeypatch.setattr(
4572
+ runner,
4573
+ "force_unlock_tei_v380_state",
4574
+ lambda: {
4575
+ "method": "terraform-force-unlock",
4576
+ "lockId": runner.TEI_V380_LOCK_ID,
4577
+ "directDynamoDbMutation": False,
4578
+ },
4579
+ )
4580
+ monkeypatch.setattr(runner.time, "sleep", lambda _seconds: None)
4581
+
4582
+ def fake_output(args, **_kwargs):
4583
+ nonlocal secret_describes, kms_describes
4584
+ calls.append(args)
4585
+ if args[1:3] == ["sts", "get-caller-identity"]:
4586
+ return "637423202447"
4587
+ if args[1:3] == ["secretsmanager", "describe-secret"]:
4588
+ secret_describes += 1
4589
+ return json.dumps(
4590
+ {
4591
+ "ARN": runner.TEI_V380_SECRET_ARN,
4592
+ "Name": runner.TEI_V380_SECRET_NAME,
4593
+ **({"DeletedDate": "2026-07-20T13:30:54Z"} if secret_describes == 1 else {}),
4594
+ }
4595
+ )
4596
+ if args[1:3] == ["kms", "describe-key"]:
4597
+ kms_describes += 1
4598
+ states = [
4599
+ ("PendingDeletion", False),
4600
+ ("Disabled", False),
4601
+ ("Enabled", True),
4602
+ ]
4603
+ key_state, enabled = states[min(kms_describes - 1, len(states) - 1)]
4604
+ return json.dumps(
4605
+ {
4606
+ "KeyMetadata": {
4607
+ "KeyId": runner.TEI_V380_KMS_KEY_ID,
4608
+ "Arn": runner.TEI_V380_KMS_KEY_ARN,
4609
+ "KeyState": key_state,
4610
+ "Enabled": enabled,
4611
+ "KeyManager": "CUSTOMER",
4612
+ "Origin": "AWS_KMS",
4613
+ }
4614
+ }
4615
+ )
4616
+ raise AssertionError(args)
4617
+
4618
+ def fake_run(args, **_kwargs):
4619
+ calls.append(args)
4620
+ return None
4621
+
4622
+ def fake_evidence(name, body):
4623
+ evidence.append((name, json.loads(json.dumps(body))))
4624
+ return {"fileName": name, "sha256": "a" * 64, "s3Uri": f"s3://evidence/{name}"}
4625
+
4626
+ monkeypatch.setattr(runner, "output", fake_output)
4627
+ monkeypatch.setattr(runner, "run", fake_run)
4628
+ monkeypatch.setattr(runner, "write_json_evidence_artifact", fake_evidence)
4629
+
4630
+ recovered = runner.recover_tei_v380_incident(payload)
4631
+
4632
+ assert recovered["proof"]["status"] == "succeeded"
4633
+ assert recovered["proof"]["secretValueRead"] is False
4634
+ assert recovered["proof"]["terraformStateMutation"] is False
4635
+ assert recovered["proof"]["terraformLock"]["directDynamoDbMutation"] is False
4636
+ assert [
4637
+ "aws",
4638
+ "secretsmanager",
4639
+ "restore-secret",
4640
+ "--secret-id",
4641
+ runner.TEI_V380_SECRET_ARN,
4642
+ ] in calls
4643
+ assert ["aws", "kms", "cancel-key-deletion", "--key-id", runner.TEI_V380_KMS_KEY_ID] in calls
4644
+ assert ["aws", "kms", "enable-key", "--key-id", runner.TEI_V380_KMS_KEY_ID] in calls
4645
+ assert not any("get-secret-value" in call for call in calls)
4646
+ assert evidence[-1][0] == "incident-recovery.json"
4647
+ assert all("SecretString" not in json.dumps(body) for _, body in evidence)
4648
+
4649
+
4650
+ def test_tei_v380_recovery_refuses_changed_state_before_mutation(
4651
+ monkeypatch: pytest.MonkeyPatch,
4652
+ ) -> None:
4653
+ runner = load_runner()
4654
+ payload = _tei_v380_recovery_payload(runner)
4655
+ changed_state = {**_tei_v380_state(runner), "serial": runner.TEI_V380_STATE_SERIAL + 1}
4656
+ mutation_calls: list[list[str]] = []
4657
+
4658
+ monkeypatch.setattr(runner, "assert_incident_recovery_operation_is_exclusive", lambda: {})
4659
+ monkeypatch.setattr(runner, "terraform_state_object_snapshot", lambda _stage: changed_state)
4660
+ monkeypatch.setattr(
4661
+ runner,
4662
+ "output",
4663
+ lambda args, **_kwargs: (
4664
+ "637423202447"
4665
+ if args[1:3] == ["sts", "get-caller-identity"]
4666
+ else (_ for _ in ()).throw(AssertionError(args))
4667
+ ),
4668
+ )
4669
+ monkeypatch.setattr(runner, "run", lambda args, **_kwargs: mutation_calls.append(args))
4670
+
4671
+ with pytest.raises(RuntimeError, match="state serial changed"):
4672
+ runner.recover_tei_v380_incident(payload)
4673
+ assert mutation_calls == []
4674
+
4675
+
4676
+ def test_tei_v380_force_unlock_uses_terraform_and_preserves_digest(
4677
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4678
+ ) -> None:
4679
+ runner = load_runner()
4680
+ monkeypatch.setattr(runner, "WORK", tmp_path)
4681
+ calls: list[tuple[list[str], dict]] = []
4682
+ snapshots = iter(
4683
+ [
4684
+ {
4685
+ "table": runner.TEI_V380_LOCK_TABLE,
4686
+ "key": runner.TEI_V380_LOCK_KEY,
4687
+ "present": True,
4688
+ "info": {
4689
+ "id": runner.TEI_V380_LOCK_ID,
4690
+ "operation": "OperationTypeApply",
4691
+ "version": "1.8.5",
4692
+ "created": "2026-07-20T13:30:42Z",
4693
+ "path": runner.TEI_V380_LOCK_KEY,
4694
+ },
4695
+ "digestKey": f"{runner.TEI_V380_LOCK_KEY}-md5",
4696
+ "digest": runner.TEI_V380_STATE_DIGEST,
4697
+ },
4698
+ {
4699
+ "table": runner.TEI_V380_LOCK_TABLE,
4700
+ "key": runner.TEI_V380_LOCK_KEY,
4701
+ "present": False,
4702
+ "info": None,
4703
+ "digestKey": f"{runner.TEI_V380_LOCK_KEY}-md5",
4704
+ "digest": runner.TEI_V380_STATE_DIGEST,
4705
+ },
4706
+ ]
4707
+ )
4708
+ monkeypatch.setattr(runner, "tei_v380_lock_snapshot", lambda: next(snapshots))
4709
+ monkeypatch.setattr(runner, "run", lambda args, **kwargs: calls.append((args, kwargs)))
4710
+
4711
+ proof = runner.force_unlock_tei_v380_state()
4712
+
4713
+ assert proof["method"] == "terraform-force-unlock"
4714
+ assert proof["directDynamoDbMutation"] is False
4715
+ assert calls[0][0][:3] == ["terraform", "init", "-reconfigure"]
4716
+ assert calls[1][0] == [
4717
+ "terraform",
4718
+ "force-unlock",
4719
+ "-force",
4720
+ runner.TEI_V380_LOCK_ID,
4721
+ ]
4722
+ assert not any("dynamodb" in arg for call, _kwargs in calls for arg in call)
4723
+ backend = (tmp_path / "tei-v380-force-unlock/backend.hcl").read_text()
4724
+ assert runner.TEI_V380_STATE_BUCKET in backend
4725
+ assert runner.TEI_V380_LOCK_TABLE in backend
4726
+
4727
+
4728
+ def test_tei_v380_force_unlock_is_retry_safe_after_exact_unlock(
4729
+ monkeypatch: pytest.MonkeyPatch,
4730
+ ) -> None:
4731
+ runner = load_runner()
4732
+ calls: list[list[str]] = []
4733
+ unlocked = {
4734
+ "table": runner.TEI_V380_LOCK_TABLE,
4735
+ "key": runner.TEI_V380_LOCK_KEY,
4736
+ "present": False,
4737
+ "info": None,
4738
+ "digestKey": f"{runner.TEI_V380_LOCK_KEY}-md5",
4739
+ "digest": runner.TEI_V380_STATE_DIGEST,
4740
+ }
4741
+ monkeypatch.setattr(runner, "tei_v380_lock_snapshot", lambda: unlocked)
4742
+ monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
4743
+
4744
+ proof = runner.force_unlock_tei_v380_state()
4745
+
4746
+ assert proof["alreadyUnlocked"] is True
4747
+ assert proof["before"] == proof["after"]
4748
+ assert calls == []
4749
+
4750
+
4751
+ def test_tei_v380_recovery_imports_require_exact_evidence_and_current_state(
4752
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4753
+ ) -> None:
4754
+ runner = load_runner()
4755
+ monkeypatch.setattr(runner, "WORK", tmp_path)
4756
+ state = _tei_v380_state(runner)
4757
+ scope = runner.tei_v380_recovery_scope()
4758
+ evidence = {
4759
+ "contract": runner.TEI_V380_RECOVERY_CONTRACT,
4760
+ "incidentId": runner.TEI_V380_RECOVERY_INCIDENT_ID,
4761
+ "rejectedPlanSha256": runner.TEI_V380_REJECTED_PLAN_SHA256,
4762
+ "status": "succeeded",
4763
+ "terraformStateMutation": False,
4764
+ "secretValueRead": False,
4765
+ "stateBefore": state,
4766
+ "stateAfter": state,
4767
+ }
4768
+ payload = {
4769
+ "incidentRecovery": {
4770
+ **scope,
4771
+ "evidence": {"s3Uri": "s3://evidence/recovery.json", "sha256": "a" * 64},
4772
+ }
4773
+ }
4774
+ vars_json = {
4775
+ "enable_agentcore_multiplayer_proof": True,
4776
+ "agentcore_multiplayer_proof_tenant_slug": "tei",
4777
+ "agentcore_multiplayer_proof_owner_allowlist": runner.TEI_V380_OWNER_ALLOWLIST,
4778
+ "finalize_auth_retirement": False,
4779
+ "auth_retirement_phase": "coexistence",
4780
+ "microsoft_oauth_tenant": "organizations",
4781
+ }
4782
+
4783
+ def fake_download(_uri, destination, _sha, _label):
4784
+ destination.write_text(json.dumps(evidence), encoding="utf-8")
4785
+ return "a" * 64
4786
+
4787
+ monkeypatch.setattr(runner, "_download_approved_artifact", fake_download)
4788
+ snapshots = [state]
4789
+ monkeypatch.setattr(
4790
+ runner,
4791
+ "terraform_state_object_snapshot",
4792
+ lambda stage: snapshots.pop(0) if stage == "tei-e2e" else {},
4793
+ )
4794
+ blocks = runner.tei_v380_recovery_import_blocks(payload, vars_json)
4795
+ assert f"to = {runner.TEI_V380_SECRET_ADDRESS}" in blocks
4796
+ assert f"id = {runner.hcl_string(runner.TEI_V380_SECRET_ARN)}" in blocks
4797
+ assert f"to = {runner.TEI_V380_KMS_KEY_ADDRESS}" in blocks
4798
+ assert runner.TEI_V380_KMS_ALIAS_ADDRESS not in blocks
4799
+
4800
+ moved_state = {**state, "sha256": "f" * 64}
4801
+ snapshots.append(moved_state)
4802
+ with pytest.raises(RuntimeError, match="state moved"):
4803
+ runner.tei_v380_recovery_import_blocks(payload, vars_json)
4804
+
4805
+ payload["incidentRecovery"]["stage"] = "dev"
4806
+ with pytest.raises(RuntimeError, match="scope does not match"):
4807
+ runner.tei_v380_recovery_import_blocks(payload, vars_json)
4808
+
4809
+
4810
+ def test_terraform_plan_summary_counts_resource_actions() -> None:
4811
+ runner = load_runner()
4812
+
4813
+ summary = runner.terraform_plan_summary(
4814
+ {
4815
+ "format_version": "1.2",
4816
+ "terraform_version": "1.8.0",
4817
+ "resource_changes": [
4818
+ {"change": {"actions": ["create"]}},
4819
+ {"change": {"actions": ["update"]}},
4820
+ {"change": {"actions": ["delete", "create"]}},
4821
+ {"change": {"actions": ["create"]}},
4822
+ ],
4823
+ }
4824
+ )
4825
+
4826
+ assert summary == {
4827
+ "formatVersion": "1.2",
4828
+ "terraformVersion": "1.8.0",
4829
+ "resourceChangeCount": 4,
4830
+ "resourceChangesByAction": {
4831
+ "create": 2,
4832
+ "update": 1,
4833
+ "delete,create": 1,
4834
+ },
4835
+ }
4836
+
4837
+
4838
+ def test_self_update_runner_script_copies_release_runner_to_s3(
4839
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4840
+ ) -> None:
4841
+ runner = load_runner()
4842
+ source_dir = tmp_path / "source"
4843
+ script = source_dir / "terraform/modules/app/deployment-control-plane/runner.py"
4844
+ script.parent.mkdir(parents=True)
4845
+ script.write_text("# release runner\n", encoding="utf-8")
4846
+
4847
+ monkeypatch.setattr(runner, "SOURCE", source_dir)
4848
+ monkeypatch.setenv(
4849
+ "THINKWORK_RUNNER_SCRIPT_S3_URI",
4850
+ "s3://evidence-bucket/runner/thinkwork-runner.py",
4851
+ )
4852
+ calls: list[list[str]] = []
4853
+ monkeypatch.setattr(runner, "run", lambda args, **_kw: calls.append(args))
4854
+
4855
+ runner.self_update_runner_script()
4856
+
4857
+ assert calls == [
4858
+ [
4859
+ "aws",
4860
+ "s3",
4861
+ "cp",
4862
+ str(script),
4863
+ "s3://evidence-bucket/runner/thinkwork-runner.py",
4864
+ ]
4865
+ ]
4866
+
4867
+
4868
+ def test_self_update_runner_script_skips_when_source_missing(
4869
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4870
+ ) -> None:
4871
+ runner = load_runner()
4872
+ monkeypatch.setattr(runner, "SOURCE", tmp_path / "missing-source")
4873
+ monkeypatch.setenv(
4874
+ "THINKWORK_RUNNER_SCRIPT_S3_URI",
4875
+ "s3://evidence-bucket/runner/thinkwork-runner.py",
4876
+ )
4877
+ calls: list[list[str]] = []
4878
+ monkeypatch.setattr(runner, "run", lambda args, **_kw: calls.append(args))
4879
+
4880
+ runner.self_update_runner_script()
4881
+
4882
+ assert calls == []
4883
+
4884
+
4885
+ # ---------------------------------------------------------------------------
4886
+ # First-admin bootstrap (first-run provisioning)
4887
+ # ---------------------------------------------------------------------------
4888
+
4889
+
4890
+ def test_first_admin_tenant_slug_prefers_customer_domain_label() -> None:
4891
+ runner = load_runner()
4892
+ slug = runner.first_admin_tenant_slug(
4893
+ {}, {}, {"stage": "acme-prod", "customer_domain": "acme.thinkwork.ai"}
4894
+ )
4895
+ assert slug == "acme"
4896
+
4897
+
4898
+ def test_first_admin_tenant_slug_falls_back_to_stage() -> None:
4899
+ runner = load_runner()
4900
+ slug = runner.first_admin_tenant_slug({}, {}, {"stage": "acme", "customer_domain": ""})
4901
+ assert slug == "acme"
4902
+
4903
+
4904
+ def test_first_admin_tenant_slug_rejects_domain_mismatch() -> None:
4905
+ runner = load_runner()
4906
+ with pytest.raises(RuntimeError, match="KTD8"):
4907
+ runner.first_admin_tenant_slug(
4908
+ {},
4909
+ {"tenantSlug": "other"},
4910
+ {"stage": "acme", "customer_domain": "acme.thinkwork.ai"},
4911
+ )
4912
+
4913
+
4914
+ def test_first_admin_tenant_slug_rejects_invalid_slug() -> None:
4915
+ runner = load_runner()
4916
+ with pytest.raises(RuntimeError, match="slug pattern"):
4917
+ runner.first_admin_tenant_slug({}, {}, {"stage": "Bad_Stage!", "customer_domain": ""})
4918
+
4919
+
4920
+ def test_first_admin_email_takes_first_valid_entry() -> None:
4921
+ runner = load_runner()
4922
+ assert (
4923
+ runner.first_admin_email({"platform_operator_emails": "ops@acme.com, two@acme.com"})
4924
+ == "ops@acme.com"
4925
+ )
4926
+ assert runner.first_admin_email({"platform_operator_emails": ""}) == ""
4927
+ assert runner.first_admin_email({"platform_operator_emails": "not-an-email"}) == ""
4928
+ assert runner.first_admin_email({"platform_operator_emails": "a'b@acme.com"}) == ""
4929
+
4930
+
4931
+ def test_ensure_first_admin_skips_without_admin_email(tmp_path: Path) -> None:
4932
+ runner = load_runner()
4933
+ outputs = tmp_path / "outputs.json"
4934
+ outputs.write_text("{}", encoding="utf-8")
4935
+ runner.ensure_first_admin(outputs, {"platform_operator_emails": "", "stage": "acme"}, {}, {})
4936
+ assert runner.FIRST_ADMIN_EVIDENCE["status"] == "skipped"
4937
+
4938
+
4939
+ def test_ensure_first_admin_skips_established_env_without_slug(
4940
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4941
+ ) -> None:
4942
+ runner = load_runner()
4943
+ outputs = tmp_path / "outputs.json"
3629
4944
  outputs.write_text(json.dumps({"user_pool_id": {"value": "us-east-1_POOL"}}), encoding="utf-8")
3630
4945
  monkeypatch.setattr(runner, "database_url_from_outputs", lambda _o: "postgres://x")
3631
4946
 
@@ -3731,3 +5046,568 @@ def test_ensure_first_admin_failure_is_nonfatal(
3731
5046
  )
3732
5047
  assert runner.FIRST_ADMIN_EVIDENCE["status"] == "failed"
3733
5048
  assert "user_pool_id" in runner.FIRST_ADMIN_EVIDENCE["error"]
5049
+
5050
+
5051
+ def test_write_runner_files_threads_deployment_runner_in_vpc(
5052
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
5053
+ ) -> None:
5054
+ """THINK-1082: the runner-in-VPC flag rides the same three wiring points
5055
+ as every other environment-posture variable — vars_json, the tfvars
5056
+ file, and the generated root's variable + module argument — and is
5057
+ ship-inert (False) unless the runner-secrets document or payload says
5058
+ otherwise."""
5059
+ runner = load_runner()
5060
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
5061
+ base_payload = {
5062
+ "stage": "tei-e2e",
5063
+ "awsRegion": "us-east-1",
5064
+ "awsAccountId": "637423202447",
5065
+ "dbPassword": "db-secret",
5066
+ "apiAuthSecret": "api-secret",
5067
+ }
5068
+
5069
+ vars_json_default = runner.write_runner_files(dict(base_payload), {})
5070
+ assert vars_json_default["deployment_runner_in_vpc"] is False
5071
+
5072
+ vars_json_payload = runner.write_runner_files(
5073
+ {**base_payload, "deploymentRunnerInVpc": True}, {}
5074
+ )
5075
+ assert vars_json_payload["deployment_runner_in_vpc"] is True
5076
+ tfvars = json.loads((tf_dir / "terraform.auto.tfvars.json").read_text(encoding="utf-8"))
5077
+ assert tfvars["deployment_runner_in_vpc"] is True
5078
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
5079
+ assert 'variable "deployment_runner_in_vpc"' in main_tf
5080
+ assert "deployment_runner_in_vpc = var.deployment_runner_in_vpc" in main_tf
5081
+
5082
+ # Secrets Manager JSON often carries strings; the runner-secrets document
5083
+ # is the durable home and wins over the payload, emitting a real bool.
5084
+ vars_json_secret = runner.write_runner_files(
5085
+ {**base_payload, "deploymentRunnerInVpc": False},
5086
+ {"deploymentRunnerInVpc": "true"},
5087
+ )
5088
+ assert vars_json_secret["deployment_runner_in_vpc"] is True
5089
+
5090
+
5091
+ def test_write_runner_files_threads_api_lambdas_in_vpc_and_db_exposure(
5092
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
5093
+ ) -> None:
5094
+ """THINK-1082: the api-Lambdas-in-VPC flag and the two database-exposure
5095
+ inputs ride the same wiring points as deploymentRunnerInVpc (vars_json,
5096
+ tfvars, generated-root variable + module argument) and are ship-inert:
5097
+ Lambdas out of the VPC, public endpoint on, world-open 5432 seeded."""
5098
+ runner = load_runner()
5099
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
5100
+ base_payload = {
5101
+ "stage": "tei-e2e",
5102
+ "awsRegion": "us-east-1",
5103
+ "awsAccountId": "637423202447",
5104
+ "dbPassword": "db-secret",
5105
+ "apiAuthSecret": "api-secret",
5106
+ }
5107
+
5108
+ vars_json_default = runner.write_runner_files(dict(base_payload), {})
5109
+ assert vars_json_default["api_lambdas_in_vpc"] is False
5110
+ assert vars_json_default["db_publicly_accessible"] is True
5111
+ assert vars_json_default["db_public_ingress_cidrs"] == ["0.0.0.0/0"]
5112
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
5113
+ for name in ("api_lambdas_in_vpc", "db_publicly_accessible", "db_public_ingress_cidrs"):
5114
+ assert f'variable "{name}"' in main_tf
5115
+ assert f"{name:<35}= var.{name}" in main_tf
5116
+
5117
+ # Payload lane: the closed posture. An explicit [] must survive as []
5118
+ # (not fall back to the world-open default) — that IS the port close.
5119
+ vars_json_closed = runner.write_runner_files(
5120
+ {
5121
+ **base_payload,
5122
+ "apiLambdasInVpc": True,
5123
+ "dbPubliclyAccessible": False,
5124
+ "dbPublicIngressCidrs": [],
5125
+ },
5126
+ {},
5127
+ )
5128
+ assert vars_json_closed["api_lambdas_in_vpc"] is True
5129
+ assert vars_json_closed["db_publicly_accessible"] is False
5130
+ assert vars_json_closed["db_public_ingress_cidrs"] == []
5131
+ tfvars = json.loads((tf_dir / "terraform.auto.tfvars.json").read_text(encoding="utf-8"))
5132
+ assert tfvars["api_lambdas_in_vpc"] is True
5133
+ assert tfvars["db_publicly_accessible"] is False
5134
+ assert tfvars["db_public_ingress_cidrs"] == []
5135
+
5136
+ # Secrets Manager JSON often carries strings; the runner-secrets document
5137
+ # is the durable home and wins over the payload, emitting real types.
5138
+ vars_json_secret = runner.write_runner_files(
5139
+ {**base_payload, "apiLambdasInVpc": False, "dbPublicIngressCidrs": ["0.0.0.0/0"]},
5140
+ {
5141
+ "apiLambdasInVpc": "true",
5142
+ "dbPubliclyAccessible": "false",
5143
+ "dbPublicIngressCidrs": "10.0.0.0/8, 192.168.0.0/16",
5144
+ },
5145
+ )
5146
+ assert vars_json_secret["api_lambdas_in_vpc"] is True
5147
+ assert vars_json_secret["db_publicly_accessible"] is False
5148
+ assert vars_json_secret["db_public_ingress_cidrs"] == ["10.0.0.0/8", "192.168.0.0/16"]
5149
+
5150
+ # An explicit empty string in the secret also means "no rule".
5151
+ vars_json_empty = runner.write_runner_files(dict(base_payload), {"dbPublicIngressCidrs": ""})
5152
+ assert vars_json_empty["db_public_ingress_cidrs"] == []
5153
+
5154
+
5155
+ def test_psql_failures_redact_the_database_password(monkeypatch: pytest.MonkeyPatch) -> None:
5156
+ """A failed psql surfaces its argv through CalledProcessError, and
5157
+ CodeBuild prints that to CloudWatch. The password must never reach the
5158
+ log; everything else about the failure (type, returncode, output) is
5159
+ preserved for callers that already catch CalledProcessError."""
5160
+ runner = load_runner()
5161
+ password = "s3cr%40t%2Fpw"
5162
+ url = (
5163
+ f"postgresql://thinkwork_admin:{password}"
5164
+ "@db.cluster-abc.us-east-1.rds.amazonaws.com:5432/thinkwork?sslmode=require"
5165
+ )
5166
+
5167
+ def failing_output(args, **_kwargs):
5168
+ raise subprocess.CalledProcessError(2, args, output="", stderr="Connection timed out")
5169
+
5170
+ monkeypatch.setattr(runner, "output", failing_output)
5171
+ with pytest.raises(subprocess.CalledProcessError) as raised:
5172
+ runner.psql_output(url, "SELECT to_regclass('public.tenants')")
5173
+ assert password not in str(raised.value)
5174
+ assert password not in json.dumps(raised.value.cmd)
5175
+ assert "thinkwork_admin:***@db.cluster-abc" in raised.value.cmd[1]
5176
+ assert raised.value.returncode == 2
5177
+ assert raised.value.stderr == "Connection timed out"
5178
+
5179
+ def failing_run(args, **_kwargs):
5180
+ raise subprocess.CalledProcessError(1, args)
5181
+
5182
+ monkeypatch.setattr(runner, "run", failing_run)
5183
+ with pytest.raises(subprocess.CalledProcessError) as raised_file:
5184
+ runner.psql(url, file=Path("0001_init.sql"), variables={"stage": "tei-e2e"})
5185
+ assert password not in str(raised_file.value)
5186
+ assert "-f" in raised_file.value.cmd and "0001_init.sql" in raised_file.value.cmd
5187
+
5188
+ def failing_subprocess_run(args, **_kwargs):
5189
+ raise subprocess.CalledProcessError(3, args)
5190
+
5191
+ monkeypatch.setattr(runner.subprocess, "run", failing_subprocess_run)
5192
+ with pytest.raises(subprocess.CalledProcessError) as raised_sql:
5193
+ runner.psql(url, sql="SELECT 1")
5194
+ assert password not in str(raised_sql.value)
5195
+ assert raised_sql.value.returncode == 3
5196
+
5197
+ assert runner.redact_database_url(url) == (
5198
+ "postgresql://thinkwork_admin:***"
5199
+ "@db.cluster-abc.us-east-1.rds.amazonaws.com:5432/thinkwork?sslmode=require"
5200
+ )
5201
+ assert runner.redact_database_url("no url here") == "no url here"
5202
+ assert runner.redact_database_url(None) is None
5203
+
5204
+
5205
+ ################################################################################
5206
+ # Database access over the RDS Data API (THINK-1082)
5207
+ ################################################################################
5208
+
5209
+ DRIZZLE_DIR = Path(__file__).resolve().parents[4] / "packages/database-pg/drizzle"
5210
+
5211
+
5212
+ class FakeRdsDataClient:
5213
+ """boto3-shaped rds-data stand-in: records every call in order, answers
5214
+ scalar probes from a tiny in-memory ledger, and can fail one statement."""
5215
+
5216
+ def __init__(self, fail_on: str | None = None, ledger: set[str] | None = None) -> None:
5217
+ self.calls: list[tuple[str, str | None, str]] = []
5218
+ self.fail_on = fail_on
5219
+ self.ledger = set(ledger or set())
5220
+ self.transactions = 0
5221
+
5222
+ def execute_statement(self, **params):
5223
+ sql = params["sql"]
5224
+ self.calls.append(("execute", params.get("transactionId"), sql))
5225
+ if self.fail_on and self.fail_on in sql:
5226
+ raise RuntimeError(f"ERROR: boom near {self.fail_on}; SQLState: 42601")
5227
+ if "to_regclass('public.tenants')" in sql:
5228
+ return {"records": [[{"stringValue": "tenants"}]]}
5229
+ if "to_regclass('public.platform_schema_migrations')" in sql:
5230
+ return {"records": [[{"stringValue": "platform_schema_migrations"}]]}
5231
+ if "SELECT name FROM public.platform_schema_migrations" in sql:
5232
+ return {"records": [[{"stringValue": name}] for name in sorted(self.ledger)]}
5233
+ if "INSERT INTO public.platform_schema_migrations" in sql:
5234
+ self.ledger.update(re.findall(r"\('([^']+)', '[^']+'\)", sql))
5235
+ return {"records": []}
5236
+
5237
+ def begin_transaction(self, **params):
5238
+ self.transactions += 1
5239
+ transaction_id = f"tx{self.transactions}"
5240
+ self.calls.append(("begin", transaction_id, ""))
5241
+ return {"transactionId": transaction_id}
5242
+
5243
+ def commit_transaction(self, **params):
5244
+ self.calls.append(("commit", params["transactionId"], ""))
5245
+ return {}
5246
+
5247
+ def rollback_transaction(self, **params):
5248
+ self.calls.append(("rollback", params["transactionId"], ""))
5249
+ return {}
5250
+
5251
+ def executed(self) -> list[str]:
5252
+ return [sql for kind, _tx, sql in self.calls if kind == "execute"]
5253
+
5254
+
5255
+ def data_api_db(runner, client: FakeRdsDataClient):
5256
+ return runner.DataApiDatabase(
5257
+ "arn:aws:rds:us-east-1:637423202447:cluster:thinkwork-tei-e2e-db",
5258
+ "arn:aws:secretsmanager:us-east-1:637423202447:secret:db-abc",
5259
+ "thinkwork",
5260
+ client=client,
5261
+ )
5262
+
5263
+
5264
+ def test_split_sql_script_honours_quotes_dollar_bodies_comments_and_breakpoints() -> None:
5265
+ runner = load_runner()
5266
+ script = """
5267
+ -- leading comment; with a semicolon
5268
+ CREATE TABLE a (id int);
5269
+ --> statement-breakpoint
5270
+ CREATE FUNCTION f() RETURNS void AS $$
5271
+ BEGIN
5272
+ PERFORM 1; PERFORM 2;
5273
+ END;
5274
+ $$ LANGUAGE plpgsql
5275
+ --> statement-breakpoint
5276
+ DO $body$ BEGIN RAISE NOTICE 'x; y'; END $body$;
5277
+ INSERT INTO a VALUES ('semi;colon'), (E'esc\\'aped; still'); /* block; comment */
5278
+ SELECT "quoted;identifier" FROM a;
5279
+ -- trailing comment only
5280
+ """
5281
+ items = runner.split_sql_script(script)
5282
+ assert [kind for kind, _ in items] == ["sql"] * 5
5283
+ statements = [text for _, text in items]
5284
+ assert statements[0].endswith("CREATE TABLE a (id int)")
5285
+ assert (
5286
+ statements[1].startswith("CREATE FUNCTION f()") and "PERFORM 1; PERFORM 2;" in statements[1]
5287
+ )
5288
+ assert statements[2] == "DO $body$ BEGIN RAISE NOTICE 'x; y'; END $body$"
5289
+ assert statements[3].startswith("INSERT INTO a VALUES ('semi;colon')")
5290
+ assert "E'esc\\'aped; still'" in statements[3]
5291
+ assert statements[4].startswith("/* block; comment */")
5292
+ assert statements[4].endswith('SELECT "quoted;identifier" FROM a')
5293
+ # the drizzle marker alone is a boundary even without a semicolon
5294
+ assert runner.split_sql_script("SELECT 1\n--> statement-breakpoint\nSELECT 2;") == [
5295
+ ("sql", "SELECT 1"),
5296
+ ("sql", "SELECT 2"),
5297
+ ]
5298
+
5299
+
5300
+ def test_split_sql_script_lifts_meta_command_lines_in_order() -> None:
5301
+ runner = load_runner()
5302
+ script = "\\set ON_ERROR_STOP on\nSELECT 1;\n\\echo 'done; really'\nSELECT 2;\n"
5303
+ assert runner.split_sql_script(script) == [
5304
+ ("meta", "\\set ON_ERROR_STOP on"),
5305
+ ("sql", "SELECT 1"),
5306
+ ("meta", "\\echo 'done; really'"),
5307
+ ("sql", "SELECT 2"),
5308
+ ]
5309
+ # a backslash inside a dollar-quoted body is not a meta-command
5310
+ body = "DO $$ BEGIN\n\\x := 1;\nEND $$;"
5311
+ assert runner.split_sql_script(body) == [("sql", "DO $$ BEGIN\n\\x := 1;\nEND $$")]
5312
+
5313
+
5314
+ def test_substitute_psql_variables_matches_psql_interpolation() -> None:
5315
+ runner = load_runner()
5316
+ variables = {"stage": "tei-e2e", "who": "o'brien"}
5317
+ sql = (
5318
+ "SELECT :'stage', :\"stage\", :stage, :'who', 1::text, ':stage untouched', "
5319
+ "\"also :'stage'\" -- and :'stage' in a comment\n, :'undefined'"
5320
+ )
5321
+ assert runner.substitute_psql_variables(sql, variables) == (
5322
+ "SELECT 'tei-e2e', \"tei-e2e\", tei-e2e, 'o''brien', 1::text, ':stage untouched', "
5323
+ "\"also :'stage'\" -- and :'stage' in a comment\n, :'undefined'"
5324
+ )
5325
+ assert runner.substitute_psql_variables("SELECT :'stage'", None) == "SELECT :'stage'"
5326
+
5327
+
5328
+ def test_data_api_plans_the_real_meta_command_migrations() -> None:
5329
+ """0031, 0036 and 0037 are the migration files that use psql
5330
+ meta-commands; every form they use is interpreted, read from disk."""
5331
+ runner = load_runner()
5332
+ db = data_api_db(runner, FakeRdsDataClient())
5333
+
5334
+ # 0036 / 0037: `\set ON_ERROR_STOP on` only.
5335
+ for name in ("0036_user_scoped_memory_wiki.sql", "0037_user_profile_operating_model.sql"):
5336
+ text = (DRIZZLE_DIR / name).read_text(encoding="utf-8")
5337
+ assert "\\set ON_ERROR_STOP on" in text
5338
+ planned = db._plan(text, name, {"stage": "tei-e2e"})
5339
+ assert planned and not any(statement.startswith("\\") for statement in planned)
5340
+
5341
+ # 0031: `\if :{?stage}` / `\else` / `\set stage dev` / `\endif`, then
5342
+ # `:'stage'` inside the S3 snapshot call.
5343
+ text = (DRIZZLE_DIR / "0031_thread_cleanup_drops.sql").read_text(encoding="utf-8")
5344
+ with_stage = db._plan(text, "0031_thread_cleanup_drops.sql", {"stage": "tei-e2e"})
5345
+ assert any("'thinkwork-' || 'tei-e2e' || '-backups'" in statement for statement in with_stage)
5346
+ without_stage = db._plan(text, "0031_thread_cleanup_drops.sql", {})
5347
+ assert any("'thinkwork-' || 'dev' || '-backups'" in statement for statement in without_stage)
5348
+
5349
+ # Anything this lane does not implement is refused by name, before any
5350
+ # statement runs.
5351
+ with pytest.raises(runner.DatabaseCommandError) as refused:
5352
+ db._plan("SELECT 1;\n\\copy a FROM 'x.csv'\n", "0999_bad.sql", {})
5353
+ assert "unsupported psql meta-command in Data API mode: \\copy a FROM 'x.csv'" in str(
5354
+ refused.value
5355
+ )
5356
+ assert "0999_bad.sql" in str(refused.value)
5357
+ with pytest.raises(runner.DatabaseCommandError):
5358
+ db._plan("\\if 1 = 1\nSELECT 1;\n\\endif\n", "0998_bad.sql", {})
5359
+
5360
+
5361
+ def test_data_api_quit_ends_the_script_like_psql() -> None:
5362
+ """0076 quits (exit 0 under psql) when computer_id is not passed — the
5363
+ runner's sweep passes only `stage`, so the file is a recorded no-op."""
5364
+ runner = load_runner()
5365
+ client = FakeRdsDataClient()
5366
+ db = data_api_db(runner, client)
5367
+ db.execute(
5368
+ file=DRIZZLE_DIR / "0076_scheduled_jobs_marco_backfill.sql", variables={"stage": "x"}
5369
+ )
5370
+ assert client.calls == []
5371
+
5372
+
5373
+ def test_data_api_scalar_renders_like_psql_tA() -> None:
5374
+ runner = load_runner()
5375
+ client = FakeRdsDataClient()
5376
+ db = data_api_db(runner, client)
5377
+ client.execute_statement = lambda **params: { # type: ignore[method-assign]
5378
+ "records": [
5379
+ [{"longValue": 3}, {"booleanValue": True}, {"isNull": True}, {"stringValue": "a"}],
5380
+ [
5381
+ {"doubleValue": 1.5},
5382
+ {"booleanValue": False},
5383
+ {"stringValue": ""},
5384
+ {"stringValue": "b"},
5385
+ ],
5386
+ ]
5387
+ }
5388
+ assert db.scalar("SELECT ...") == "3|t||a\n1.5|f||b"
5389
+ client.execute_statement = lambda **params: {"records": []} # type: ignore[method-assign]
5390
+ assert db.scalar("SELECT to_regclass('nothing')::text") == ""
5391
+ assert runner.psql_output(db, "SELECT 1") == ""
5392
+
5393
+
5394
+ def test_data_api_stops_at_the_first_error_and_rolls_back() -> None:
5395
+ runner = load_runner()
5396
+ client = FakeRdsDataClient(fail_on="SECOND")
5397
+ db = data_api_db(runner, client)
5398
+ with pytest.raises(subprocess.CalledProcessError) as raised:
5399
+ runner.psql(db, sql="SELECT 'first'; SELECT 'SECOND'; SELECT 'third';")
5400
+ assert isinstance(raised.value, runner.DatabaseCommandError)
5401
+ assert raised.value.returncode == 1
5402
+ assert "statement 2 (SELECT 'SECOND'): ERROR: boom" in str(raised.value)
5403
+ assert "inline sql" in raised.value.cmd
5404
+ assert [kind for kind, _tx, _sql in client.calls] == ["begin", "execute", "execute", "rollback"]
5405
+ assert not any("third" in sql for sql in client.executed())
5406
+
5407
+
5408
+ def test_data_api_wraps_scripts_in_one_transaction_and_maps_explicit_ones() -> None:
5409
+ """Every script runs inside a Data API transaction so its statements
5410
+ serialise on one connection (a statement that outlives its 45 s call can
5411
+ never race the next). A script's own BEGIN/COMMIT map onto it, and
5412
+ CREATE INDEX CONCURRENTLY — which refuses a transaction block — commits
5413
+ the open one and runs standalone."""
5414
+ runner = load_runner()
5415
+ client = FakeRdsDataClient()
5416
+ db = data_api_db(runner, client)
5417
+ db.execute(
5418
+ sql=(
5419
+ "BEGIN;\nSET LOCAL lock_timeout = '5s';\nUPDATE a SET b = 1;\nCOMMIT;\n"
5420
+ "CREATE INDEX CONCURRENTLY IF NOT EXISTS i ON a (b);\n"
5421
+ "ALTER TABLE a VALIDATE CONSTRAINT c;\n"
5422
+ )
5423
+ )
5424
+ assert [(kind, tx) for kind, tx, _sql in client.calls] == [
5425
+ ("begin", "tx1"),
5426
+ ("execute", "tx1"),
5427
+ ("execute", "tx1"),
5428
+ ("commit", "tx1"),
5429
+ ("execute", None),
5430
+ ("begin", "tx2"),
5431
+ ("execute", "tx2"),
5432
+ ("commit", "tx2"),
5433
+ ]
5434
+ ddl_call = client.calls[1]
5435
+ assert ddl_call[2].startswith("SET LOCAL")
5436
+
5437
+
5438
+ def test_data_api_ships_oversized_statements_through_a_temp_table() -> None:
5439
+ """0230_analyst_rls carries an 85 KB DO block — over the Data API's
5440
+ 64 KB per-statement limit. It is delivered in pieces to a temp table on
5441
+ the transaction's connection, reassembled and run there with EXECUTE."""
5442
+ runner = load_runner()
5443
+ client = FakeRdsDataClient()
5444
+ db = data_api_db(runner, client)
5445
+ db.execute(file=DRIZZLE_DIR / "0230_analyst_rls.sql", variables={"stage": "tei-e2e"})
5446
+ executed = client.executed()
5447
+ assert all(len(sql.encode("utf-8")) <= runner.RDS_DATA_API_MAX_SQL_BYTES for sql in executed)
5448
+ inserts = [
5449
+ sql for sql in executed if sql.startswith("INSERT INTO pg_temp.runner_oversized_statement")
5450
+ ]
5451
+ assert len(inserts) == 2
5452
+ assembled = "".join(
5453
+ re.search(r"\$chunk\$(.*)\$chunk\$\)$", sql, re.S).group(1) for sql in inserts
5454
+ )
5455
+ assert assembled.lstrip().startswith("--") and "END $$" in assembled
5456
+ assert any(sql.startswith("DO $runner$ BEGIN EXECUTE") for sql in executed)
5457
+ assert executed[-1] == "DROP TABLE pg_temp.runner_oversized_statement"
5458
+ assert len({tx for kind, tx, _ in client.calls if kind == "execute"}) == 1 # one transaction
5459
+ assert [kind for kind, _tx, _sql in client.calls][-1] == "commit"
5460
+
5461
+
5462
+ def test_write_runner_files_threads_db_access_as_a_runner_only_flag(
5463
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
5464
+ ) -> None:
5465
+ """THINK-1082: dbAccess rides the runner-secrets / payload precedence
5466
+ like apiLambdasInVpc, defaults to psql, and is runner-only: it never
5467
+ lands in terraform.auto.tfvars.json or the generated root."""
5468
+ runner = load_runner()
5469
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
5470
+ base_payload = {
5471
+ "stage": "tei-e2e",
5472
+ "awsRegion": "us-east-1",
5473
+ "awsAccountId": "637423202447",
5474
+ "dbPassword": "db-secret",
5475
+ "apiAuthSecret": "api-secret",
5476
+ }
5477
+ vars_json_default = runner.write_runner_files(dict(base_payload), {})
5478
+ assert vars_json_default["db_access"] == "psql"
5479
+ tfvars = json.loads((tf_dir / "terraform.auto.tfvars.json").read_text(encoding="utf-8"))
5480
+ assert "db_access" not in tfvars
5481
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
5482
+ assert "db_access" not in main_tf
5483
+ assert 'output "db_cluster_arn" { value = module.thinkwork.db_cluster_arn }' in main_tf
5484
+
5485
+ vars_json_payload = runner.write_runner_files({**base_payload, "dbAccess": "data-api"}, {})
5486
+ assert vars_json_payload["db_access"] == "data-api"
5487
+ vars_json_secret = runner.write_runner_files(
5488
+ {**base_payload, "dbAccess": "psql"}, {"dbAccess": "Data-API"}
5489
+ )
5490
+ assert vars_json_secret["db_access"] == "data-api"
5491
+ with pytest.raises(RuntimeError, match="dbAccess must be one of psql, data-api"):
5492
+ runner.write_runner_files({**base_payload, "dbAccess": "tunnel"}, {})
5493
+
5494
+ assert runner.set_database_access_mode(vars_json_payload) == "data-api"
5495
+ assert runner.DATABASE_ACCESS_MODE == "data-api"
5496
+ assert runner.set_database_access_mode({}) == "psql"
5497
+
5498
+
5499
+ def test_push_database_schema_over_the_data_api_applies_and_records_pending_files(
5500
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
5501
+ ) -> None:
5502
+ """With dbAccess=data-api the whole schema step — probes, ledger, the
5503
+ pending files and their ledger rows — runs through the fake rds-data
5504
+ client, in order, with each file recorded only after its statements."""
5505
+ runner = load_runner()
5506
+ client = FakeRdsDataClient(ledger={"0001_init.sql", "0155_tenant_model_catalog.sql"})
5507
+ source_dir = tmp_path / "source"
5508
+ write_drizzle_files(
5509
+ source_dir,
5510
+ ["0001_init.sql", "0002_add_a.sql", "0003_add_b.sql", "0155_tenant_model_catalog.sql"],
5511
+ )
5512
+ drizzle = source_dir / "packages/database-pg/drizzle"
5513
+ (drizzle / "0002_add_a.sql").write_text(
5514
+ "\\set ON_ERROR_STOP on\nBEGIN;\nCREATE TABLE a (id int);\n"
5515
+ "SELECT set_config('thinkwork.stage', :'stage', true);\nCOMMIT;\n",
5516
+ encoding="utf-8",
5517
+ )
5518
+ (drizzle / "0003_add_b.sql").write_text(
5519
+ "CREATE TABLE b (id int);\n--> statement-breakpoint\nCREATE INDEX b_id ON b (id);\n",
5520
+ encoding="utf-8",
5521
+ )
5522
+ outputs_path = tmp_path / "outputs.json"
5523
+ outputs_path.write_text(
5524
+ json.dumps(
5525
+ {
5526
+ "db_cluster_arn": {
5527
+ "value": "arn:aws:rds:us-east-1:637423202447:cluster:thinkwork-tei-e2e-db"
5528
+ },
5529
+ "db_cluster_endpoint": {
5530
+ "value": "thinkwork-tei-e2e-db.cluster-x.rds.amazonaws.com"
5531
+ },
5532
+ "db_secret_arn": {"value": "arn:aws:secretsmanager:us-east-1:1:secret:db"},
5533
+ "database_name": {"value": "thinkwork"},
5534
+ }
5535
+ ),
5536
+ encoding="utf-8",
5537
+ )
5538
+ monkeypatch.setattr(runner, "SOURCE", source_dir)
5539
+ monkeypatch.setenv("THINKWORK_TERRAFORM_MODULE_SOURCE", "thinkwork-ai/thinkwork/aws")
5540
+ monkeypatch.setattr(runner, "checkout_source", lambda *_args: None)
5541
+ monkeypatch.setattr(runner, "rds_data_client", lambda _region: client)
5542
+ monkeypatch.setattr(
5543
+ runner,
5544
+ "seed_platform_bootstrap_defaults",
5545
+ lambda _db: client.calls.append(("seed", None, "")),
5546
+ )
5547
+ runner.set_database_access_mode({"db_access": "data-api"})
5548
+
5549
+ runner.push_database_schema(outputs_path, {"stage": "tei-e2e"})
5550
+
5551
+ executed = client.executed()
5552
+ assert executed[0] == "SELECT to_regclass('public.tenants')::text"
5553
+ assert "CREATE TABLE IF NOT EXISTS public.platform_schema_migrations" in executed[2]
5554
+ order = [
5555
+ next(i for i, sql in enumerate(executed) if sql == "CREATE TABLE a (id int)"),
5556
+ next(
5557
+ i for i, sql in enumerate(executed) if "set_config('thinkwork.stage', 'tei-e2e'" in sql
5558
+ ),
5559
+ next(i for i, sql in enumerate(executed) if "('0002_add_a.sql', 'runner')" in sql),
5560
+ next(i for i, sql in enumerate(executed) if sql == "CREATE TABLE b (id int)"),
5561
+ next(i for i, sql in enumerate(executed) if sql == "CREATE INDEX b_id ON b (id)"),
5562
+ next(i for i, sql in enumerate(executed) if "('0003_add_b.sql', 'runner')" in sql),
5563
+ ]
5564
+ assert order == sorted(order)
5565
+ assert not any("('0001_init.sql'" in sql for sql in executed)
5566
+ assert client.ledger == {
5567
+ "0001_init.sql",
5568
+ "0002_add_a.sql",
5569
+ "0003_add_b.sql",
5570
+ "0155_tenant_model_catalog.sql",
5571
+ }
5572
+ kinds = [kind for kind, _tx, _sql in client.calls]
5573
+ assert kinds.count("begin") == kinds.count("commit") and "rollback" not in kinds
5574
+ assert kinds.index("seed") > order[-1]
5575
+
5576
+ # A failing file is never recorded: the error surfaces as the
5577
+ # CalledProcessError callers already handle, after a rollback.
5578
+ failing = FakeRdsDataClient(
5579
+ ledger={"0001_init.sql", "0002_add_a.sql", "0155_tenant_model_catalog.sql"},
5580
+ fail_on="CREATE INDEX b_id",
5581
+ )
5582
+ monkeypatch.setattr(runner, "rds_data_client", lambda _region: failing)
5583
+ with pytest.raises(subprocess.CalledProcessError) as raised:
5584
+ runner.push_database_schema(outputs_path, {"stage": "tei-e2e"})
5585
+ assert "0003_add_b.sql" in raised.value.cmd
5586
+ assert failing.ledger == {"0001_init.sql", "0002_add_a.sql", "0155_tenant_model_catalog.sql"}
5587
+ assert [kind for kind, _tx, _sql in failing.calls][-1] == "rollback"
5588
+
5589
+
5590
+ def test_data_api_database_from_outputs_derives_the_cluster_arn_when_missing(
5591
+ monkeypatch: pytest.MonkeyPatch,
5592
+ ) -> None:
5593
+ runner = load_runner()
5594
+ monkeypatch.setattr(runner, "output", lambda _args, **_kw: "637423202447")
5595
+ db = runner.DataApiDatabase.from_outputs(
5596
+ {
5597
+ "db_cluster_endpoint": {
5598
+ "value": "thinkwork-tei-e2e-db.cluster-abc.us-east-1.rds.amazonaws.com"
5599
+ },
5600
+ "db_secret_arn": {"value": "arn:aws:secretsmanager:us-east-1:637423202447:secret:db"},
5601
+ }
5602
+ )
5603
+ assert db.cluster_arn == "arn:aws:rds:us-east-1:637423202447:cluster:thinkwork-tei-e2e-db"
5604
+ assert db.database_name == "thinkwork"
5605
+ assert "secret" not in repr(db).lower() or "arn:aws:secretsmanager" not in repr(db)
5606
+ runner.set_database_access_mode({"db_access": "data-api"})
5607
+ handle = runner.database_url_from_outputs(
5608
+ {
5609
+ "db_cluster_arn": {"value": db.cluster_arn},
5610
+ "db_secret_arn": {"value": db.secret_arn},
5611
+ }
5612
+ )
5613
+ assert isinstance(handle, runner.DataApiDatabase)