thinkwork-cli 0.13.1 → 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 (262) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
  3. package/dist/bowser-6Z6JFAIR.js +2822 -0
  4. package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
  5. package/dist/chunk-AD2NIWB5.js +6516 -0
  6. package/dist/chunk-DV3MRBR5.js +43 -0
  7. package/dist/chunk-EI5ZX7CN.js +522 -0
  8. package/dist/chunk-EUJ7X6YD.js +126 -0
  9. package/dist/chunk-GG5WKBOX.js +436 -0
  10. package/dist/chunk-I64EZSU5.js +358 -0
  11. package/dist/chunk-KMIJ5KGT.js +128 -0
  12. package/dist/chunk-MWQ7RO76.js +137 -0
  13. package/dist/chunk-OCPHU34C.js +996 -0
  14. package/dist/chunk-OLUQ4U2Z.js +1728 -0
  15. package/dist/chunk-XYM7PE2A.js +2264 -0
  16. package/dist/chunk-YQCYD6NE.js +3299 -0
  17. package/dist/cli.js +49156 -31257
  18. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  19. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  20. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  21. package/dist/dist-es-2OTNTL4N.js +315 -0
  22. package/dist/dist-es-2XVAH4BQ.js +14726 -0
  23. package/dist/dist-es-3DPDUJAH.js +2000 -0
  24. package/dist/dist-es-BNZTBBVC.js +176 -0
  25. package/dist/dist-es-CD2RBDAC.js +88 -0
  26. package/dist/dist-es-I4FW57PS.js +88 -0
  27. package/dist/dist-es-KL7Y5NB2.js +377 -0
  28. package/dist/dist-es-KVFWJNDY.js +318 -0
  29. package/dist/dist-es-PS73DWNL.js +70 -0
  30. package/dist/dist-es-S3NZQFGT.js +170 -0
  31. package/dist/dist-es-SGCA4P32.js +48 -0
  32. package/dist/dist-es-WBM6J4RA.js +70 -0
  33. package/dist/dist-es-ZGVADYIF.js +485 -0
  34. package/dist/dist-es-ZKC2QKD2.js +377 -0
  35. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  36. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  37. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  38. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  39. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  40. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  41. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  42. package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
  43. package/dist/drizzle/0230_analyst_rls.sql +28 -36
  44. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  45. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  46. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  47. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  48. package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
  49. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  50. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  51. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  52. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  53. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  54. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  55. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  56. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  57. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  58. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  59. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  60. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  61. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  62. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  63. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  64. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  65. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  66. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  67. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  68. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  69. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  70. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  71. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  72. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  73. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  74. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  75. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  76. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  77. package/dist/drizzle/0287_brain_subject.sql +25 -0
  78. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  79. package/dist/event-streams-7JNLJEEL.js +894 -0
  80. package/dist/event-streams-KDF5RWR6.js +1515 -0
  81. package/dist/loadSso-ETCXTR4I.js +576 -0
  82. package/dist/loadSso-UBD7KUHE.js +583 -0
  83. package/dist/plugins/README.md +2 -2
  84. package/dist/plugins/catalog/package.json +1 -5
  85. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
  86. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  87. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
  88. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
  89. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
  90. package/dist/plugins/catalog/src/contracts.ts +17 -179
  91. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
  92. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  93. package/dist/plugins/twenty/package.json +2 -1
  94. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  95. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  96. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  97. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  98. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  99. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  100. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  101. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  102. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  103. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  104. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  105. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  106. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  107. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  108. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  109. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  110. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  111. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  112. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  113. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  114. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  115. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  116. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  117. package/dist/scripts/post-deploy.sh +79 -1
  118. package/dist/signin-5QZNRSOO.js +821 -0
  119. package/dist/sso-oidc-XK673P54.js +837 -0
  120. package/dist/sts-OXI2UALF.js +1113 -0
  121. package/dist/sts-SQW4NBK3.js +1117 -0
  122. package/dist/terraform/examples/greenfield/main.tf +391 -176
  123. package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
  124. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  125. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  126. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  127. package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
  128. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  129. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
  130. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
  131. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  132. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  133. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  134. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
  135. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  136. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
  137. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
  138. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  139. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  140. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  141. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  142. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  143. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  144. package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
  145. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  146. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  147. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  148. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  149. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  150. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  151. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  152. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  153. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  154. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  155. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  156. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  157. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  158. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  159. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  160. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  161. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  162. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  163. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  164. package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
  165. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
  166. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  167. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  168. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  169. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  170. package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
  171. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
  172. package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
  173. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  174. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  175. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
  176. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  177. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  178. package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
  179. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  180. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  181. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  182. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  183. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  184. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  185. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  186. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  187. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  188. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  189. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  190. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  191. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  192. package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
  193. package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
  194. package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
  195. package/dist/terraform/modules/thinkwork/README.md +5 -1
  196. package/dist/terraform/modules/thinkwork/main.tf +459 -234
  197. package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
  198. package/dist/terraform/modules/thinkwork/variables.tf +353 -89
  199. package/dist/terraform/schema.graphql +69 -36
  200. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  201. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  202. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  203. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  204. package/dist/workspace-defaults/files/USER.md +1 -0
  205. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  206. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  207. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  208. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  209. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  210. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  211. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  212. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  213. package/package.json +3 -2
  214. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  215. package/dist/plugins/company-data/README.md +0 -49
  216. package/dist/plugins/company-data/package.json +0 -21
  217. package/dist/plugins/company-data/src/index.ts +0 -30
  218. package/dist/plugins/company-data/src/manifest.ts +0 -26
  219. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  220. package/dist/plugins/company-data/tsconfig.json +0 -8
  221. package/dist/plugins/company-etl/README.md +0 -46
  222. package/dist/plugins/company-etl/package.json +0 -21
  223. package/dist/plugins/company-etl/src/index.ts +0 -29
  224. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  225. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  226. package/dist/plugins/company-etl/tsconfig.json +0 -8
  227. package/dist/plugins/lastmile/README.md +0 -50
  228. package/dist/plugins/lastmile/package.json +0 -23
  229. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  230. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  231. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  232. package/dist/plugins/lastmile/src/index.ts +0 -46
  233. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  234. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  235. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  236. package/dist/plugins/lastmile/tsconfig.json +0 -8
  237. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  238. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  239. package/dist/plugins/workos-auth/package.json +0 -22
  240. package/dist/plugins/workos-auth/src/index.ts +0 -30
  241. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  242. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  243. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  244. package/dist/plugins/workos-auth/tsconfig.json +0 -8
  245. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  246. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  247. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  248. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  249. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
  250. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
  251. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
  252. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  253. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  254. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  255. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  256. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  257. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  258. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  259. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  260. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  261. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  262. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -8,6 +8,7 @@ import subprocess
8
8
  import tarfile
9
9
  from hashlib import sha256
10
10
  from pathlib import Path
11
+ from types import SimpleNamespace
11
12
 
12
13
  import pytest
13
14
 
@@ -125,18 +126,635 @@ def test_customer_update_rejects_non_customer_agentcore_pi_source_image() -> Non
125
126
  def test_customer_update_accepts_customer_ecr_agentcore_pi_source_image() -> None:
126
127
  runner = load_runner()
127
128
  image = (
128
- "637423202447.dkr.ecr.us-east-1.amazonaws.com/"
129
- "thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
129
+ "637423202447.dkr.ecr.us-east-1.amazonaws.com/thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
130
130
  )
131
131
 
132
- assert runner.resolve_agentcore_pi_source_image_uri(
132
+ assert (
133
+ runner.resolve_agentcore_pi_source_image_uri(
134
+ {
135
+ "action": "update",
136
+ "awsAccountId": "637423202447",
137
+ "awsRegion": "us-east-1",
138
+ "agentcorePiSourceImageUri": image,
139
+ }
140
+ )
141
+ == image
142
+ )
143
+
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
+
408
+ def test_native_auth_reconciliation_builds_route_specific_secret_free_manifest(
409
+ monkeypatch: pytest.MonkeyPatch,
410
+ ) -> None:
411
+ runner = load_runner()
412
+ outputs = {
413
+ "user_pool_id": {"value": "us-east-1_Example123"},
414
+ "auth_route_clients": {
415
+ "value": {
416
+ "web:local": {
417
+ "client_id": "1234567890abcdefghijklmnop",
418
+ "route_key": "local",
419
+ "client_family": "web",
420
+ "provider_names": ["COGNITO"],
421
+ "explicit_auth_flows": [
422
+ "ALLOW_USER_PASSWORD_AUTH",
423
+ "ALLOW_USER_SRP_AUTH",
424
+ "ALLOW_REFRESH_TOKEN_AUTH",
425
+ ],
426
+ "callback_urls": ["https://app.example/auth/callback"],
427
+ "logout_urls": ["https://app.example"],
428
+ "lifecycle_state": "native",
429
+ },
430
+ "web:google": {
431
+ "client_id": "abcdefghijklmnopqrstuvwxyz",
432
+ "route_key": "google",
433
+ "client_family": "web",
434
+ "provider_names": ["Google"],
435
+ "explicit_auth_flows": ["ALLOW_REFRESH_TOKEN_AUTH"],
436
+ "callback_urls": ["https://app.example/auth/callback"],
437
+ "logout_urls": ["https://app.example"],
438
+ "lifecycle_state": "native",
439
+ },
440
+ "web:microsoft": {
441
+ "client_id": "zyxwvutsrqponmlkjihgfedcba",
442
+ "route_key": "microsoft",
443
+ "client_family": "web",
444
+ "provider_names": ["MicrosoftOrganizations"],
445
+ "explicit_auth_flows": ["ALLOW_REFRESH_TOKEN_AUTH"],
446
+ "callback_urls": ["https://app.example/auth/callback"],
447
+ "logout_urls": ["https://app.example"],
448
+ "lifecycle_state": "native",
449
+ },
450
+ }
451
+ },
452
+ }
453
+ scope = {
454
+ "stage": "dev",
455
+ "account_id": "123456789012",
456
+ "region": "us-east-1",
457
+ "microsoft_oauth_tenant": "00000000-0000-4000-8000-000000000123",
458
+ }
459
+
460
+ payload, state = runner.native_auth_reconciliation_payload(outputs, scope, {})
461
+
462
+ assert payload["revision"] == 1
463
+ assert [item["connectionKey"] for item in payload["connections"]] == [
464
+ "google",
465
+ "local",
466
+ "microsoft:organizations",
467
+ ]
468
+ by_route = {item["routeKey"]: item for item in payload["routeClients"]}
469
+ assert by_route["local"]["providerNames"] == ["COGNITO"]
470
+ assert by_route["google"]["providerNames"] == ["Google"]
471
+ assert by_route["microsoft"]["providerNames"] == ["MicrosoftOrganizations"]
472
+ microsoft_connection = next(
473
+ item
474
+ for item in payload["connections"]
475
+ if item["connectionKey"] == "microsoft:organizations"
476
+ )
477
+ assert microsoft_connection["issuerUrl"] == (
478
+ "https://login.microsoftonline.com/00000000-0000-4000-8000-000000000123/v2.0"
479
+ )
480
+ assert "secret" not in json.dumps(payload).lower()
481
+ assert len(payload["manifestFingerprint"]) == 64
482
+
483
+ retry_payload, _ = runner.native_auth_reconciliation_payload(outputs, scope, {})
484
+ assert retry_payload["idempotencyKey"] == payload["idempotencyKey"]
485
+ assert retry_payload["manifestFingerprint"] == payload["manifestFingerprint"]
486
+
487
+ replay, replay_state = runner.native_auth_reconciliation_payload(outputs, scope, state)
488
+ assert replay is None
489
+ assert replay_state == state
490
+
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
+
540
+ def tenant_entra_operation(action: str, revision: int = 1) -> dict:
541
+ tenant_id = "00000000-0000-4000-8000-000000000123"
542
+ payload = {
543
+ "action": "update",
544
+ "operation": {
545
+ "kind": "identity_provider",
546
+ "action": action,
547
+ "revision": revision,
548
+ "expectedPreviousRevision": revision - 1,
549
+ "connection": {
550
+ "connectionKey": f"microsoft:tenant:{tenant_id}",
551
+ "tenantId": tenant_id,
552
+ "providerName": "Entra_0000000000004000_deadbeef",
553
+ "displayName": "Acme Microsoft",
554
+ "clientId": "entra-client-id",
555
+ "clientSecretRef": (
556
+ "arn:aws:secretsmanager:us-east-1:123456789012:"
557
+ f"secret:thinkwork/dev/auth/entra/{tenant_id}-ABC123"
558
+ ),
559
+ "issuerUrl": f"https://login.microsoftonline.com/{tenant_id}/v2.0",
560
+ "tenantBindings": [
561
+ {
562
+ "tenantId": "019f762b-683c-7153-acff-24ee932d73f6",
563
+ "label": "Acme",
564
+ "hostnames": ["Login.Acme.Example", "login.acme.example"],
565
+ }
566
+ ],
567
+ },
568
+ },
569
+ }
570
+ if action == "rotate":
571
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWSPENDING"
572
+ return payload
573
+
574
+
575
+ def test_identity_provider_operation_builds_revisioned_secret_free_desired_state() -> None:
576
+ runner = load_runner()
577
+ payload = tenant_entra_operation("create")
578
+
579
+ desired = runner.identity_provider_desired_connections(
580
+ payload,
581
+ stage="dev",
582
+ account_id="123456789012",
583
+ region="us-east-1",
584
+ previous_state={},
585
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
586
+ )
587
+
588
+ assert len(desired) == 1
589
+ assert desired[0]["lifecycleState"] == "native"
590
+ assert desired[0]["tenantDirectoryId"] == "00000000-0000-4000-8000-000000000123"
591
+ assert desired[0]["tenantBindings"][0]["hostnames"] == ["login.acme.example"]
592
+ assert runner.tenant_entra_terraform_projection(desired) == [
133
593
  {
134
- "action": "update",
135
- "awsAccountId": "637423202447",
136
- "awsRegion": "us-east-1",
137
- "agentcorePiSourceImageUri": image,
594
+ "connection_key": "microsoft:tenant:00000000-0000-4000-8000-000000000123",
595
+ "tenant_id": "00000000-0000-4000-8000-000000000123",
596
+ "provider_name": "Entra_0000000000004000_deadbeef",
597
+ "display_name": "Acme Microsoft",
138
598
  }
139
- ) == image
599
+ ]
600
+ assert "tenant-super-secret" not in json.dumps(desired)
601
+
602
+
603
+ def test_identity_provider_disable_removes_route_before_provider_retirement() -> None:
604
+ runner = load_runner()
605
+ created = runner.identity_provider_desired_connections(
606
+ tenant_entra_operation("create"),
607
+ stage="dev",
608
+ account_id="123456789012",
609
+ region="us-east-1",
610
+ previous_state={},
611
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
612
+ )
613
+ disabled = runner.identity_provider_desired_connections(
614
+ tenant_entra_operation("disable", revision=2),
615
+ stage="dev",
616
+ account_id="123456789012",
617
+ region="us-east-1",
618
+ previous_state={"desiredRevision": 1, "tenantConnections": created},
619
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
620
+ )
621
+
622
+ assert disabled[0]["lifecycleState"] == "denied"
623
+ assert disabled[0]["tenantBindings"][0]["status"] == "disabled"
624
+ assert runner.tenant_entra_terraform_projection(disabled) == []
625
+
626
+
627
+ def test_identity_provider_operation_rejects_cross_stage_secret_reference() -> None:
628
+ runner = load_runner()
629
+ payload = tenant_entra_operation("create")
630
+ payload["operation"]["connection"]["clientSecretRef"] = (
631
+ "arn:aws:secretsmanager:us-east-1:123456789012:secret:thinkwork/prod/auth/entra/example"
632
+ )
633
+
634
+ with pytest.raises(RuntimeError, match="account, region, stage"):
635
+ runner.identity_provider_desired_connections(
636
+ payload,
637
+ stage="dev",
638
+ account_id="123456789012",
639
+ region="us-east-1",
640
+ previous_state={},
641
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
642
+ )
643
+
644
+
645
+ def test_identity_provider_operation_rejects_invalid_secret_version_stage() -> None:
646
+ runner = load_runner()
647
+ payload = tenant_entra_operation("rotate", revision=2)
648
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWS_PENDING"
649
+
650
+ with pytest.raises(RuntimeError, match="must be AWSPENDING"):
651
+ runner.identity_provider_desired_connections(
652
+ payload,
653
+ stage="dev",
654
+ account_id="123456789012",
655
+ region="us-east-1",
656
+ previous_state={"desiredRevision": 1, "tenantConnections": []},
657
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
658
+ )
659
+
660
+
661
+ def test_identity_provider_operation_rejects_pending_stage_outside_rotation() -> None:
662
+ runner = load_runner()
663
+ payload = tenant_entra_operation("create")
664
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWSPENDING"
665
+
666
+ with pytest.raises(RuntimeError, match="valid only.*rotation"):
667
+ runner.identity_provider_desired_connections(
668
+ payload,
669
+ stage="dev",
670
+ account_id="123456789012",
671
+ region="us-east-1",
672
+ previous_state={},
673
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
674
+ )
675
+
676
+
677
+ def test_identity_provider_rotation_requires_pending_stage() -> None:
678
+ runner = load_runner()
679
+ created = runner.identity_provider_desired_connections(
680
+ tenant_entra_operation("create"),
681
+ stage="dev",
682
+ account_id="123456789012",
683
+ region="us-east-1",
684
+ previous_state={},
685
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
686
+ )
687
+ payload = tenant_entra_operation("rotate", revision=2)
688
+ payload["operation"]["connection"].pop("clientSecretVersionStage")
689
+
690
+ with pytest.raises(RuntimeError, match="requires clientSecretVersionStage AWSPENDING"):
691
+ runner.identity_provider_desired_connections(
692
+ payload,
693
+ stage="dev",
694
+ account_id="123456789012",
695
+ region="us-east-1",
696
+ previous_state={"desiredRevision": 1, "tenantConnections": created},
697
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
698
+ )
699
+
700
+
701
+ def test_identity_provider_update_does_not_require_agent_image_pin() -> None:
702
+ runner = load_runner()
703
+
704
+ assert (
705
+ runner.resolve_agentcore_pi_source_image_uri(
706
+ {"action": "update", "operation": {"kind": "identity_provider"}}
707
+ )
708
+ == ""
709
+ )
710
+
711
+
712
+ def test_identity_provider_rotate_reads_pending_secret_and_promotes_only_after_reconcile(
713
+ monkeypatch: pytest.MonkeyPatch,
714
+ ) -> None:
715
+ runner = load_runner()
716
+ connection = runner.identity_provider_desired_connections(
717
+ tenant_entra_operation("rotate", revision=2),
718
+ stage="dev",
719
+ account_id="123456789012",
720
+ region="us-east-1",
721
+ previous_state={
722
+ "desiredRevision": 1,
723
+ "tenantConnections": runner.identity_provider_desired_connections(
724
+ tenant_entra_operation("create"),
725
+ stage="dev",
726
+ account_id="123456789012",
727
+ region="us-east-1",
728
+ previous_state={},
729
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
730
+ ),
731
+ },
732
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
733
+ )[0]
734
+ calls = []
735
+
736
+ def fake_output(args, **_kwargs):
737
+ calls.append(args)
738
+ if "get-secret-value" in args:
739
+ return json.dumps({"clientId": "entra-client-id", "clientSecret": "pending-secret"})
740
+ if "describe-secret" in args:
741
+ return json.dumps(
742
+ {"pending-version": ["AWSPENDING"], "current-version": ["AWSCURRENT"]}
743
+ )
744
+ return "{}"
745
+
746
+ monkeypatch.setattr(runner, "output", fake_output)
747
+ assert runner._read_tenant_entra_secret(connection) == "pending-secret"
748
+ assert "AWSPENDING" in calls[0]
749
+
750
+ vars_json = {"auth_tenant_connection_metadata": [connection]}
751
+ promoted = runner.promote_tenant_entra_pending_secret(
752
+ tenant_entra_operation("rotate", revision=2), vars_json
753
+ )
754
+ assert promoted["status"] == "promoted"
755
+ promotion = next(args for args in calls if "update-secret-version-stage" in args)
756
+ assert "pending-version" in promotion
757
+ assert "current-version" in promotion
140
758
 
141
759
 
142
760
  def test_bootstrap_can_fall_back_to_release_agentcore_pi_image(
@@ -146,9 +764,11 @@ def test_bootstrap_can_fall_back_to_release_agentcore_pi_image(
146
764
  monkeypatch.setattr(
147
765
  runner,
148
766
  "release_runtime_image",
149
- lambda name: "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
150
- if name == "agentcore-pi-amd64"
151
- else "",
767
+ lambda name: (
768
+ "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
769
+ if name == "agentcore-pi-amd64"
770
+ else ""
771
+ ),
152
772
  )
153
773
 
154
774
  assert runner.resolve_agentcore_pi_source_image_uri({"action": "deploy"}) == (
@@ -230,53 +850,261 @@ def test_sync_release_artifacts_stages_artifacts_from_platform_bundle(
230
850
  assert {artifact["source"] for artifact in runner.RELEASE_EVIDENCE["artifacts"]} == {"bundle"}
231
851
 
232
852
 
233
- def test_ensure_release_manifest_available_accepts_manifest_file_digest(
853
+ def test_packaged_agentcore_control_runtime_is_exact_and_runnable(
234
854
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
235
855
  ) -> None:
236
856
  runner = load_runner()
237
- manifest_path = tmp_path / "thinkwork-release.json"
238
- downloaded_path = tmp_path / "downloaded-release.json"
239
- bundle_path = tmp_path / "platform-artifacts.tar.gz"
240
- write_tar(bundle_path, {})
241
- manifest = release_manifest(bundle_path, runner.sha256_file(bundle_path), [])
242
- manifest_sha = write_manifest(manifest_path, manifest)
243
- canonical_sha = runner.release_manifest_sha256(manifest)
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
+ )
244
870
 
245
- assert digest(manifest_path.read_bytes()) == manifest_sha
246
- assert canonical_sha != manifest_sha
871
+ monkeypatch.setattr(runner, "RELEASE", release_dir)
872
+ monkeypatch.setattr(runner, "RELEASE_EVIDENCE", {})
873
+ monkeypatch.delenv("THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR", raising=False)
247
874
 
248
- monkeypatch.setattr(runner, "MANIFEST", downloaded_path)
875
+ evidence = runner.prepare_agentcore_control_runtime()
249
876
 
250
- runner.ensure_release_manifest_available(file_url(manifest_path), manifest_sha)
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"])
251
892
 
252
- assert downloaded_path.exists()
253
- assert digest(downloaded_path.read_bytes()) == manifest_sha
254
893
 
255
- downloaded_path.unlink()
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()
256
897
 
257
- with pytest.raises(RuntimeError, match="Release manifest digest mismatch"):
258
- runner.ensure_release_manifest_available(file_url(manifest_path), canonical_sha)
898
+ assert runner.validate_agentcore_node_runtime(version).startswith("v")
259
899
 
260
900
 
261
- def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
262
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
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,
263
912
  ) -> None:
264
913
  runner = load_runner()
265
- release_dir = tmp_path / "release-work"
266
- manifest_path = tmp_path / "thinkwork-release.json"
267
- bundle_path = tmp_path / "platform-artifacts.tar.gz"
268
- lambda_bytes = b"lambda-zip"
269
- web_bytes = b"web-tarball"
270
- docs_bytes = b"docs-tarball"
271
- write_tar(
272
- bundle_path,
273
- {
274
- "lambdas/graphql-http.zip": lambda_bytes,
275
- "static/web.tar.gz": web_bytes,
276
- "static/docs.tar.gz": docs_bytes,
277
- },
914
+
915
+ assert runner.requires_agentcore_control_runtime(
916
+ action,
917
+ {"enableAgentCoreHarness": False},
918
+ None,
278
919
  )
279
- artifacts = [
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
+
1061
+ def test_ensure_release_manifest_available_accepts_manifest_file_digest(
1062
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1063
+ ) -> None:
1064
+ runner = load_runner()
1065
+ manifest_path = tmp_path / "thinkwork-release.json"
1066
+ downloaded_path = tmp_path / "downloaded-release.json"
1067
+ bundle_path = tmp_path / "platform-artifacts.tar.gz"
1068
+ write_tar(bundle_path, {})
1069
+ manifest = release_manifest(bundle_path, runner.sha256_file(bundle_path), [])
1070
+ manifest_sha = write_manifest(manifest_path, manifest)
1071
+ canonical_sha = runner.release_manifest_sha256(manifest)
1072
+
1073
+ assert digest(manifest_path.read_bytes()) == manifest_sha
1074
+ assert canonical_sha != manifest_sha
1075
+
1076
+ monkeypatch.setattr(runner, "MANIFEST", downloaded_path)
1077
+
1078
+ runner.ensure_release_manifest_available(file_url(manifest_path), manifest_sha)
1079
+
1080
+ assert downloaded_path.exists()
1081
+ assert digest(downloaded_path.read_bytes()) == manifest_sha
1082
+
1083
+ downloaded_path.unlink()
1084
+
1085
+ with pytest.raises(RuntimeError, match="Release manifest digest mismatch"):
1086
+ runner.ensure_release_manifest_available(file_url(manifest_path), canonical_sha)
1087
+
1088
+
1089
+ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
1090
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1091
+ ) -> None:
1092
+ runner = load_runner()
1093
+ release_dir = tmp_path / "release-work"
1094
+ manifest_path = tmp_path / "thinkwork-release.json"
1095
+ bundle_path = tmp_path / "platform-artifacts.tar.gz"
1096
+ lambda_bytes = b"lambda-zip"
1097
+ web_bytes = b"web-tarball"
1098
+ extra_bytes = b"extra-tarball"
1099
+ write_tar(
1100
+ bundle_path,
1101
+ {
1102
+ "lambdas/graphql-http.zip": lambda_bytes,
1103
+ "static/web.tar.gz": web_bytes,
1104
+ "static/extra.tar.gz": extra_bytes,
1105
+ },
1106
+ )
1107
+ artifacts = [
280
1108
  {
281
1109
  "name": "graphql-http",
282
1110
  "type": "lambda",
@@ -296,13 +1124,13 @@ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
296
1124
  "sizeBytes": len(web_bytes),
297
1125
  },
298
1126
  {
299
- "name": "docs",
1127
+ "name": "extra",
300
1128
  "type": "static-site",
301
- "fileName": "docs.tar.gz",
302
- "relativePath": "static/docs.tar.gz",
1129
+ "fileName": "extra.tar.gz",
1130
+ "relativePath": "static/extra.tar.gz",
303
1131
  "url": None,
304
- "sha256": digest(docs_bytes),
305
- "sizeBytes": len(docs_bytes),
1132
+ "sha256": digest(extra_bytes),
1133
+ "sizeBytes": len(extra_bytes),
306
1134
  },
307
1135
  ]
308
1136
  manifest_sha = write_manifest(
@@ -320,10 +1148,12 @@ def test_sync_release_artifacts_can_materialize_only_web_static_bundle(
320
1148
  monkeypatch.setenv("THINKWORK_RELEASE_ARTIFACT_BUCKET", "thinkwork-artifacts")
321
1149
  monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
322
1150
 
323
- static_files = runner.sync_release_artifacts(
324
- artifact_types={"static-site"},
325
- artifact_names={"web"},
326
- )
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)
327
1157
 
328
1158
  assert static_files == {"web": release_dir / "static/web.tar.gz"}
329
1159
  assert static_files["web"].read_bytes() == web_bytes
@@ -624,6 +1454,61 @@ def run_push_database_schema(
624
1454
  return db.events
625
1455
 
626
1456
 
1457
+ def test_prepare_additive_schema_before_app_applies_data_before_app_retirement(
1458
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1459
+ ) -> None:
1460
+ runner = load_runner()
1461
+ commands: list[list[str]] = []
1462
+ pushed_vars: list[dict] = []
1463
+
1464
+ def fake_run(command, **_kwargs):
1465
+ commands.append(command)
1466
+ return SimpleNamespace(returncode=0)
1467
+
1468
+ monkeypatch.setattr(runner, "TF", tmp_path)
1469
+ monkeypatch.setattr(runner, "TERRAFORM_EVIDENCE", {})
1470
+ monkeypatch.setattr(runner.subprocess, "run", fake_run)
1471
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: "{}")
1472
+ monkeypatch.setattr(
1473
+ runner,
1474
+ "push_database_schema",
1475
+ lambda _outputs_path, vars_json: pushed_vars.append(vars_json),
1476
+ )
1477
+
1478
+ result = runner.prepare_additive_schema_before_app(
1479
+ {"action": "deploy"},
1480
+ {
1481
+ "stage": "prod",
1482
+ "auth_retirement_phase": "retired",
1483
+ "finalize_auth_retirement": True,
1484
+ },
1485
+ )
1486
+
1487
+ assert result.returncode == 0
1488
+ assert commands == [
1489
+ [
1490
+ "terraform",
1491
+ "plan",
1492
+ "-target=module.thinkwork.module.database",
1493
+ "-out=tfplan-data",
1494
+ "-no-color",
1495
+ ],
1496
+ ["terraform", "apply", "-auto-approve", "-no-color", "tfplan-data"],
1497
+ ]
1498
+ assert pushed_vars == [
1499
+ {
1500
+ "stage": "prod",
1501
+ "auth_retirement_phase": "retired",
1502
+ "finalize_auth_retirement": False,
1503
+ }
1504
+ ]
1505
+ assert runner.TERRAFORM_EVIDENCE["schemaOrdering"] == {
1506
+ "status": "succeeded",
1507
+ "target": "module.thinkwork.module.database",
1508
+ "phase": "foundation-data-additive-before-app",
1509
+ }
1510
+
1511
+
627
1512
  def test_push_database_schema_checks_out_release_selected_module_source(
628
1513
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
629
1514
  ) -> None:
@@ -710,6 +1595,47 @@ def test_push_database_schema_applies_only_unrecorded_migrations(
710
1595
  )
711
1596
 
712
1597
 
1598
+ @pytest.mark.parametrize(
1599
+ ("phase", "finalize", "expected"),
1600
+ [
1601
+ ("coexistence", False, []),
1602
+ ("coexistence", True, []),
1603
+ ("cutover", False, []),
1604
+ ("cutover", True, []),
1605
+ ("retired", False, []),
1606
+ ("retired", True, ["0263_drop_workos_auth_runtime.sql"]),
1607
+ ],
1608
+ )
1609
+ def test_push_database_schema_requires_retired_phase_and_explicit_finalization(
1610
+ tmp_path: Path,
1611
+ monkeypatch: pytest.MonkeyPatch,
1612
+ phase: str,
1613
+ finalize: bool,
1614
+ expected: list[str],
1615
+ ) -> None:
1616
+ runner = load_runner()
1617
+ monkeypatch.setattr(runner, "POST_SEED_MIGRATIONS", [])
1618
+ db = FakeLedgerDb(tenants_exist=True, ledger=set())
1619
+ run_push_database_schema(
1620
+ runner,
1621
+ tmp_path,
1622
+ monkeypatch,
1623
+ db,
1624
+ ["0263_drop_workos_auth_runtime.sql"],
1625
+ migration_bodies={
1626
+ "0263_drop_workos_auth_runtime.sql": ("-- deployment-phase: auth-retired\nSELECT 1;\n")
1627
+ },
1628
+ vars_json={
1629
+ "stage": "tei-e2e",
1630
+ "auth_retirement_phase": phase,
1631
+ "finalize_auth_retirement": finalize,
1632
+ },
1633
+ )
1634
+
1635
+ assert db.applied_files == expected
1636
+ assert ("0263_drop_workos_auth_runtime.sql" in db.ledger) is bool(expected)
1637
+
1638
+
713
1639
  def test_push_database_schema_transition_backfills_ledger_via_markers(
714
1640
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
715
1641
  ) -> None:
@@ -747,6 +1673,40 @@ def test_push_database_schema_transition_backfills_ledger_via_markers(
747
1673
  assert db.ledger["0001_ancient.sql"] == "transition-assumed"
748
1674
 
749
1675
 
1676
+ def test_push_database_schema_transition_applies_post_cutoff_migrations(
1677
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1678
+ ) -> None:
1679
+ runner = load_runner()
1680
+ db = FakeLedgerDb(
1681
+ tenants_exist=True,
1682
+ ledger=None,
1683
+ present_objects={"public.existing_native_auth_table"},
1684
+ )
1685
+ run_push_database_schema(
1686
+ runner,
1687
+ tmp_path,
1688
+ monkeypatch,
1689
+ db,
1690
+ [
1691
+ "0155_tenant_model_catalog.sql",
1692
+ "0261_native_auth_control_plane.sql",
1693
+ "0262_auth_subscription_tickets.sql",
1694
+ ],
1695
+ migration_bodies={
1696
+ "0261_native_auth_control_plane.sql": (
1697
+ "-- creates: public.missing_native_auth_table\nSELECT 1;\n"
1698
+ ),
1699
+ "0262_auth_subscription_tickets.sql": (
1700
+ "-- creates: public.existing_native_auth_table\nSELECT 1;\n"
1701
+ ),
1702
+ },
1703
+ )
1704
+
1705
+ assert "0261_native_auth_control_plane.sql" in db.applied_files
1706
+ assert db.ledger["0261_native_auth_control_plane.sql"] == "runner"
1707
+ assert db.ledger["0262_auth_subscription_tickets.sql"] == "transition-verified"
1708
+
1709
+
750
1710
  def test_push_database_schema_greenfield_records_full_ledger(
751
1711
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
752
1712
  ) -> None:
@@ -1028,6 +1988,13 @@ def test_write_runner_files_persists_selected_release_to_controller_module(
1028
1988
  assert "deployment_release_manifest_signature_url" in main_tf
1029
1989
  assert "deployment_release_manifest_trusted_keys_json" in main_tf
1030
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
1031
1998
 
1032
1999
 
1033
2000
  def _cognito_email_runner_env(runner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
@@ -1057,6 +2024,7 @@ def _cognito_email_runner_env(runner, tmp_path: Path, monkeypatch: pytest.Monkey
1057
2024
  monkeypatch.setenv("THINKWORK_TERRAFORM_LOCK_TABLE", "thinkwork-locks")
1058
2025
  monkeypatch.setenv("THINKWORK_RELEASE_ARTIFACT_BUCKET", "thinkwork-artifacts")
1059
2026
  monkeypatch.setenv("THINKWORK_RELEASE_VERSION", "v0.1.0-canary.150")
2027
+ monkeypatch.setenv("AUTH_MIGRATION_RECOVERY_DEADLINE", "2099-01-01T00:00:00Z")
1060
2028
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_URL", file_url(manifest_path))
1061
2029
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_SHA256", manifest_sha)
1062
2030
  return tf_dir
@@ -1114,6 +2082,44 @@ def test_write_runner_files_threads_cognito_email_vars_from_payload(
1114
2082
  assert f"= var.{name}" in main_tf
1115
2083
 
1116
2084
 
2085
+ def test_write_runner_files_threads_agentcore_harness_config_from_preserved_payload(
2086
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2087
+ ) -> None:
2088
+ runner = load_runner()
2089
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2090
+
2091
+ vars_json = runner.write_runner_files(
2092
+ {
2093
+ "stage": "tei-e2e",
2094
+ "awsRegion": "us-east-1",
2095
+ "awsAccountId": "637423202447",
2096
+ "dbPassword": "db-secret",
2097
+ "apiAuthSecret": "api-secret",
2098
+ "preservedConfig": {
2099
+ "enableAgentCoreHarness": True,
2100
+ "agentCoreHarnessTenantSlug": "tei",
2101
+ "agentCoreHarnessOwnerAllowlist": "owner-a,owner-b",
2102
+ },
2103
+ },
2104
+ {},
2105
+ )
2106
+
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"
2110
+
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
2121
+
2122
+
1117
2123
  def test_write_runner_files_repins_stale_thinkwork_git_module_source_to_release(
1118
2124
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1119
2125
  ) -> None:
@@ -1163,9 +2169,17 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1163
2169
  "apiAuthSecret": "api-secret",
1164
2170
  "cognitoEmailSourceArn": "arn:aws:ses:us-east-1:637423202447:identity/payload.example.com",
1165
2171
  },
1166
- {"cognitoEmailSourceArn": secret_arn},
2172
+ {
2173
+ "cognitoEmailSourceArn": secret_arn,
2174
+ "authRetirementPhase": "cutover",
2175
+ },
1167
2176
  )
1168
2177
  assert vars_json["cognito_email_source_arn"] == secret_arn
2178
+ assert vars_json["auth_retirement_phase"] == "cutover"
2179
+ assert vars_json["finalize_auth_retirement"] is False
2180
+ assert "auth_retirement_phase = var.auth_retirement_phase" in (runner.TF / "main.tf").read_text(
2181
+ encoding="utf-8"
2182
+ )
1169
2183
 
1170
2184
  vars_json_default = runner.write_runner_files(
1171
2185
  {
@@ -1182,45 +2196,86 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1182
2196
  assert vars_json_default["cognito_reply_to_email_address"] == ""
1183
2197
  assert vars_json_default["app_domain"] == ""
1184
2198
  assert vars_json_default["app_certificate_arn"] == ""
2199
+ assert vars_json_default["auth_retirement_phase"] == "retired"
2200
+ assert vars_json_default["finalize_auth_retirement"] is False
2201
+
2202
+ vars_json_retired = runner.write_runner_files(
2203
+ {
2204
+ "stage": "tei-e2e",
2205
+ "awsRegion": "us-east-1",
2206
+ "awsAccountId": "637423202447",
2207
+ "dbPassword": "db-secret",
2208
+ "apiAuthSecret": "api-secret",
2209
+ "authRetirementPhase": "retired",
2210
+ "finalizeAuthRetirement": True,
2211
+ },
2212
+ {},
2213
+ )
2214
+ assert vars_json_retired["auth_retirement_phase"] == "retired"
2215
+ assert vars_json_retired["finalize_auth_retirement"] is True
1185
2216
 
1186
2217
 
1187
- def test_write_runner_files_wires_analyst_lambda_vpc_egress(
2218
+ def test_write_runner_files_threads_additional_admin_callback_urls(
1188
2219
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1189
2220
  ) -> None:
1190
- """analyst_lambda_vpc_egress needs all three wiring points (vars_json,
1191
- root variable, module argument) and reads runner-secrets first — the
1192
- durable per-environment home for the flag — with a payload fallback.
1193
- Secrets Manager JSON strings ("true") must land as real booleans."""
1194
2221
  runner = load_runner()
1195
- tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1196
- base_payload = {
1197
- "stage": "tei-e2e",
1198
- "awsRegion": "us-east-1",
1199
- "awsAccountId": "637423202447",
1200
- "dbPassword": "db-secret",
1201
- "apiAuthSecret": "api-secret",
1202
- }
2222
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1203
2223
 
1204
- assert runner.write_runner_files(base_payload, {})[
1205
- "analyst_lambda_vpc_egress"
1206
- ] is False
1207
- assert runner.write_runner_files(
1208
- base_payload, {"analystLambdaVpcEgress": "true"}
1209
- )["analyst_lambda_vpc_egress"] is True
1210
- assert runner.write_runner_files(
1211
- {**base_payload, "analystLambdaVpcEgress": True}, {}
1212
- )["analyst_lambda_vpc_egress"] is True
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
1213
2250
 
1214
- main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
1215
- assert 'variable "analyst_lambda_vpc_egress"' in main_tf
1216
- assert "= var.analyst_lambda_vpc_egress" in main_tf
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"] == []
1217
2264
 
1218
2265
 
1219
- def test_write_runner_files_threads_customer_domain_vars_from_payload(
2266
+ def test_write_runner_files_defaults_existing_preauth_stack_to_coexistence(
1220
2267
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1221
2268
  ) -> None:
1222
2269
  runner = load_runner()
1223
- tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2270
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2271
+ monkeypatch.setattr(
2272
+ runner,
2273
+ "current_terraform_state",
2274
+ lambda _stage: {
2275
+ "outputs": {"user_pool_id": {"value": "us-east-1_Existing"}},
2276
+ "resources": [{"type": "aws_cognito_user_pool", "name": "main"}],
2277
+ },
2278
+ )
1224
2279
 
1225
2280
  vars_json = runner.write_runner_files(
1226
2281
  {
@@ -1229,8 +2284,91 @@ def test_write_runner_files_threads_customer_domain_vars_from_payload(
1229
2284
  "awsAccountId": "637423202447",
1230
2285
  "dbPassword": "db-secret",
1231
2286
  "apiAuthSecret": "api-secret",
1232
- "customerDomain": "tei.thinkwork.ai",
1233
- "customerDomainDelegated": True,
2287
+ },
2288
+ {},
2289
+ )
2290
+
2291
+ assert vars_json["auth_retirement_phase"] == "coexistence"
2292
+ assert vars_json["auth_migration_recovery_deadline"] == "2099-01-01T00:00:00Z"
2293
+
2294
+
2295
+ def test_write_runner_files_requires_deadline_for_coexistence(
2296
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2297
+ ) -> None:
2298
+ runner = load_runner()
2299
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2300
+ monkeypatch.delenv("AUTH_MIGRATION_RECOVERY_DEADLINE")
2301
+
2302
+ with pytest.raises(RuntimeError, match="authMigrationRecoveryDeadline is required"):
2303
+ runner.write_runner_files(
2304
+ {
2305
+ "stage": "tei-e2e",
2306
+ "awsRegion": "us-east-1",
2307
+ "awsAccountId": "637423202447",
2308
+ "dbPassword": "db-secret",
2309
+ "apiAuthSecret": "api-secret",
2310
+ "authRetirementPhase": "coexistence",
2311
+ },
2312
+ {},
2313
+ )
2314
+
2315
+
2316
+ def test_write_runner_files_preserves_deployed_auth_retirement_phase(
2317
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2318
+ ) -> None:
2319
+ runner = load_runner()
2320
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2321
+ monkeypatch.setattr(
2322
+ runner,
2323
+ "current_terraform_state",
2324
+ lambda _stage: {
2325
+ "outputs": {
2326
+ "auth_retirement_phase": {"value": "cutover"},
2327
+ "user_pool_id": {"value": "us-east-1_Existing"},
2328
+ }
2329
+ },
2330
+ )
2331
+
2332
+ vars_json = runner.write_runner_files(
2333
+ {
2334
+ "stage": "tei-e2e",
2335
+ "awsRegion": "us-east-1",
2336
+ "awsAccountId": "637423202447",
2337
+ "dbPassword": "db-secret",
2338
+ "apiAuthSecret": "api-secret",
2339
+ },
2340
+ {},
2341
+ )
2342
+
2343
+ assert vars_json["auth_retirement_phase"] == "cutover"
2344
+
2345
+
2346
+ def test_main_reconciles_native_auth_before_destructive_schema() -> None:
2347
+ source = Path(__file__).with_name("runner.py").read_text(encoding="utf-8")
2348
+ post_apply = source[source.index("def main():") :]
2349
+ reconciliation = post_apply.index(
2350
+ 'CONTROLLER_EVIDENCE["authReconciliation"] = reconcile_native_auth_metadata'
2351
+ )
2352
+ schema = post_apply.index("push_database_schema(outputs_path, vars_json)", reconciliation)
2353
+
2354
+ assert reconciliation < schema
2355
+
2356
+
2357
+ def test_write_runner_files_threads_customer_domain_vars_from_payload(
2358
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2359
+ ) -> None:
2360
+ runner = load_runner()
2361
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
2362
+
2363
+ vars_json = runner.write_runner_files(
2364
+ {
2365
+ "stage": "tei-e2e",
2366
+ "awsRegion": "us-east-1",
2367
+ "awsAccountId": "637423202447",
2368
+ "dbPassword": "db-secret",
2369
+ "apiAuthSecret": "api-secret",
2370
+ "customerDomain": "tei.thinkwork.ai",
2371
+ "customerDomainDelegated": True,
1234
2372
  "customerDomainLegacyRetired": False,
1235
2373
  },
1236
2374
  {},
@@ -2814,7 +3952,6 @@ def test_runtime_profile_contains_customer_authority_metadata(
2814
3952
  "app_url": {"value": "https://app.example.com"},
2815
3953
  "appsync_api_url": {"value": "https://appsync.example.com/graphql"},
2816
3954
  "appsync_realtime_url": {"value": "wss://appsync.example.com/graphql"},
2817
- "appsync_api_key": {"value": "api-key"},
2818
3955
  "auth_domain": {"value": "thinkwork-tei-e2e"},
2819
3956
  "user_pool_id": {"value": "us-east-1_abc"},
2820
3957
  "admin_client_id": {"value": "client-id"},
@@ -2858,79 +3995,816 @@ def test_redacted_tfvars_removes_secret_values() -> None:
2858
3995
  "google_oauth_client_secret": "secret-google",
2859
3996
  }
2860
3997
  )
2861
-
2862
- assert redacted == {
2863
- "stage": "tei-e2e",
2864
- "db_password": "[redacted]",
2865
- "api_auth_secret": "[redacted]",
2866
- "google_oauth_client_secret": "[redacted]",
2867
- }
3998
+
3999
+ assert redacted == {
4000
+ "stage": "tei-e2e",
4001
+ "db_password": "[redacted]",
4002
+ "api_auth_secret": "[redacted]",
4003
+ "google_oauth_client_secret": "[redacted]",
4004
+ }
4005
+
4006
+
4007
+ def test_write_controller_release_selection_to_ssm_persists_selected_release(
4008
+ monkeypatch: pytest.MonkeyPatch,
4009
+ ) -> None:
4010
+ runner = load_runner()
4011
+ calls: list[list[str]] = []
4012
+
4013
+ monkeypatch.setenv("THINKWORK_SSM_PREFIX", "/thinkwork/tei-e2e/deployment")
4014
+ monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
4015
+
4016
+ selected = runner.write_controller_release_selection_to_ssm(
4017
+ {
4018
+ "deployment_release_version": "v0.1.0-canary.147",
4019
+ "deployment_release_manifest_url": "https://example.test/thinkwork-release.json",
4020
+ "deployment_release_manifest_sha256": "f" * 64,
4021
+ "deployment_release_manifest_signature_url": "",
4022
+ "deployment_release_manifest_trust_policy": "allow_unsigned_canary",
4023
+ "deployment_release_manifest_trusted_keys_json": "[]",
4024
+ "deployment_terraform_module_source": "thinkwork-ai/thinkwork/aws",
4025
+ "deployment_terraform_module_version": "",
4026
+ }
4027
+ )
4028
+
4029
+ written = {call[7]: call[9] for call in calls}
4030
+ assert selected["terraform-module-version"] == "0.1.0-canary.147"
4031
+ assert written == {
4032
+ "/thinkwork/tei-e2e/deployment/selected-release-version": "v0.1.0-canary.147",
4033
+ "/thinkwork/tei-e2e/deployment/selected-release-manifest-url": (
4034
+ "https://example.test/thinkwork-release.json"
4035
+ ),
4036
+ "/thinkwork/tei-e2e/deployment/selected-release-manifest-sha256": "f" * 64,
4037
+ "/thinkwork/tei-e2e/deployment/selected-release-trust-policy": "allow_unsigned_canary",
4038
+ "/thinkwork/tei-e2e/deployment/selected-release-trusted-keys-json": "[]",
4039
+ "/thinkwork/tei-e2e/deployment/terraform-module-source": "thinkwork-ai/thinkwork/aws",
4040
+ "/thinkwork/tei-e2e/deployment/terraform-module-version": "0.1.0-canary.147",
4041
+ }
4042
+
4043
+
4044
+ def test_write_controller_release_selection_to_ssm_skips_empty_git_module_version(
4045
+ monkeypatch: pytest.MonkeyPatch,
4046
+ ) -> None:
4047
+ runner = load_runner()
4048
+ calls: list[list[str]] = []
4049
+
4050
+ monkeypatch.setenv("THINKWORK_SSM_PREFIX", "/thinkwork/dev/deployment")
4051
+ monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
4052
+
4053
+ selected = runner.write_controller_release_selection_to_ssm(
4054
+ {
4055
+ "deployment_release_version": "v0.1.0-canary.147",
4056
+ "deployment_release_manifest_url": "https://example.test/thinkwork-release.json",
4057
+ "deployment_release_manifest_sha256": "f" * 64,
4058
+ "deployment_release_manifest_trust_policy": "allow_unsigned_canary",
4059
+ "deployment_release_manifest_trusted_keys_json": "[]",
4060
+ "deployment_terraform_module_source": (
4061
+ "git::https://github.com/thinkwork-ai/thinkwork.git"
4062
+ "//terraform/modules/thinkwork?ref=37d7246"
4063
+ ),
4064
+ "deployment_terraform_module_version": "",
4065
+ }
4066
+ )
4067
+
4068
+ written_names = [call[7] for call in calls]
4069
+ assert "terraform-module-version" not in selected
4070
+ assert "/thinkwork/dev/deployment/terraform-module-version" not in written_names
4071
+
4072
+
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:
4107
+ runner = load_runner()
4108
+ payload = _approval_payload("plan", plan=True, apply=False)
4109
+
4110
+ assert runner.validate_terraform_execution_phase(payload, "plan") == "plan"
4111
+
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",
4120
+ }
4121
+
4122
+ assert runner.validate_terraform_execution_phase(payload, "plan") == "plan"
4123
+
4124
+
4125
+ def test_explicit_plan_execution_never_invokes_terraform_apply(
4126
+ monkeypatch: pytest.MonkeyPatch,
4127
+ ) -> None:
4128
+ runner = load_runner()
4129
+ calls: list[list[str]] = []
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
+ )
4140
+
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
+ )
4148
+
4149
+ assert result.returncode == 0
4150
+ assert calls == [["terraform", "plan", "-out=tfplan", "-no-color"]]
4151
+ assert runner.TERRAFORM_EVIDENCE["plan"]["status"] == "awaiting-approval"
4152
+
4153
+
4154
+ def test_legacy_execution_still_applies_its_successful_plan(
4155
+ monkeypatch: pytest.MonkeyPatch,
4156
+ ) -> None:
4157
+ runner = load_runner()
4158
+ calls: list[list[str]] = []
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
+ )
4169
+
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
+ )
4177
+
4178
+ assert calls == [
4179
+ ["terraform", "plan", "-out=tfplan", "-no-color"],
4180
+ ["terraform", "apply", "-auto-approve", "-no-color", "tfplan"],
4181
+ ]
4182
+
4183
+
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)
4188
+
4189
+ assert runner.validate_terraform_execution_phase(payload, action) == "legacy"
4190
+
4191
+
4192
+ def test_legacy_and_approved_apply_reconcile_native_auth_schema() -> None:
4193
+ runner = load_runner()
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
4207
+ )
4208
+
4209
+
4210
+ def test_apply_phase_requires_separate_approved_plan() -> None:
4211
+ runner = load_runner()
4212
+ payload = _approval_payload("apply", plan=False, apply=True)
4213
+
4214
+ with pytest.raises(RuntimeError, match="requires approvedPlan"):
4215
+ runner.validate_terraform_execution_phase(payload, "apply")
4216
+
4217
+ payload["approvedPlan"] = {
4218
+ "descriptor": {"s3Uri": "s3://evidence/descriptor.json", "sha256": "a" * 64}
4219
+ }
4220
+ assert runner.validate_terraform_execution_phase(payload, "apply") == "apply"
4221
+
4222
+
4223
+ def test_plan_and_apply_share_only_immutable_deployment_config_digest() -> None:
4224
+ runner = load_runner()
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
+ }
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)
4233
+
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:
4241
+ runner = load_runner()
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"]),
4315
+ )
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
4326
+
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:
4351
+ runner = load_runner()
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")
4362
+
4363
+
4364
+ def test_tei_v380_saved_plan_apply_refuses_any_delete_action(
4365
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
4366
+ ) -> None:
4367
+ runner = load_runner()
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
2868
4726
 
2869
4727
 
2870
- def test_write_controller_release_selection_to_ssm_persists_selected_release(
4728
+ def test_tei_v380_force_unlock_is_retry_safe_after_exact_unlock(
2871
4729
  monkeypatch: pytest.MonkeyPatch,
2872
4730
  ) -> None:
2873
4731
  runner = load_runner()
2874
4732
  calls: list[list[str]] = []
2875
-
2876
- monkeypatch.setenv("THINKWORK_SSM_PREFIX", "/thinkwork/tei-e2e/deployment")
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)
2877
4742
  monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
2878
4743
 
2879
- selected = runner.write_controller_release_selection_to_ssm(
2880
- {
2881
- "deployment_release_version": "v0.1.0-canary.147",
2882
- "deployment_release_manifest_url": "https://example.test/thinkwork-release.json",
2883
- "deployment_release_manifest_sha256": "f" * 64,
2884
- "deployment_release_manifest_signature_url": "",
2885
- "deployment_release_manifest_trust_policy": "allow_unsigned_canary",
2886
- "deployment_release_manifest_trusted_keys_json": "[]",
2887
- "deployment_terraform_module_source": "thinkwork-ai/thinkwork/aws",
2888
- "deployment_terraform_module_version": "",
2889
- }
2890
- )
4744
+ proof = runner.force_unlock_tei_v380_state()
2891
4745
 
2892
- written = {call[7]: call[9] for call in calls}
2893
- assert selected["terraform-module-version"] == "0.1.0-canary.147"
2894
- assert written == {
2895
- "/thinkwork/tei-e2e/deployment/selected-release-version": "v0.1.0-canary.147",
2896
- "/thinkwork/tei-e2e/deployment/selected-release-manifest-url": (
2897
- "https://example.test/thinkwork-release.json"
2898
- ),
2899
- "/thinkwork/tei-e2e/deployment/selected-release-manifest-sha256": "f" * 64,
2900
- "/thinkwork/tei-e2e/deployment/selected-release-trust-policy": "allow_unsigned_canary",
2901
- "/thinkwork/tei-e2e/deployment/selected-release-trusted-keys-json": "[]",
2902
- "/thinkwork/tei-e2e/deployment/terraform-module-source": "thinkwork-ai/thinkwork/aws",
2903
- "/thinkwork/tei-e2e/deployment/terraform-module-version": "0.1.0-canary.147",
2904
- }
4746
+ assert proof["alreadyUnlocked"] is True
4747
+ assert proof["before"] == proof["after"]
4748
+ assert calls == []
2905
4749
 
2906
4750
 
2907
- def test_write_controller_release_selection_to_ssm_skips_empty_git_module_version(
2908
- monkeypatch: pytest.MonkeyPatch,
4751
+ def test_tei_v380_recovery_imports_require_exact_evidence_and_current_state(
4752
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2909
4753
  ) -> None:
2910
4754
  runner = load_runner()
2911
- calls: list[list[str]] = []
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
+ }
2912
4782
 
2913
- monkeypatch.setenv("THINKWORK_SSM_PREFIX", "/thinkwork/dev/deployment")
2914
- monkeypatch.setattr(runner, "run", lambda args, **_kwargs: calls.append(args))
4783
+ def fake_download(_uri, destination, _sha, _label):
4784
+ destination.write_text(json.dumps(evidence), encoding="utf-8")
4785
+ return "a" * 64
2915
4786
 
2916
- selected = runner.write_controller_release_selection_to_ssm(
2917
- {
2918
- "deployment_release_version": "v0.1.0-canary.147",
2919
- "deployment_release_manifest_url": "https://example.test/thinkwork-release.json",
2920
- "deployment_release_manifest_sha256": "f" * 64,
2921
- "deployment_release_manifest_trust_policy": "allow_unsigned_canary",
2922
- "deployment_release_manifest_trusted_keys_json": "[]",
2923
- "deployment_terraform_module_source": (
2924
- "git::https://github.com/thinkwork-ai/thinkwork.git"
2925
- "//terraform/modules/thinkwork?ref=37d7246"
2926
- ),
2927
- "deployment_terraform_module_version": "",
2928
- }
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 {},
2929
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
2930
4799
 
2931
- written_names = [call[7] for call in calls]
2932
- assert "terraform-module-version" not in selected
2933
- assert "/thinkwork/dev/deployment/terraform-module-version" not in written_names
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)
2934
4808
 
2935
4809
 
2936
4810
  def test_terraform_plan_summary_counts_resource_actions() -> None:
@@ -3172,3 +5046,568 @@ def test_ensure_first_admin_failure_is_nonfatal(
3172
5046
  )
3173
5047
  assert runner.FIRST_ADMIN_EVIDENCE["status"] == "failed"
3174
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)