@@ -16,6 +16,149 @@ AUTHORITY="$ROOT/bin/fm-worker-authority.py"
1616DOC=" $ROOT /docs/azure-workers.md"
1717SUB=11111111-1111-4111-8111-111111111111
1818
19+ service_complete_front_door () {
20+ local tmp help
21+ fm_test_tmproot_into tmp fm-worker-service-complete-front-door
22+ mkdir -p " $tmp /home"
23+ help=$( FM_HOME=" $tmp /home" " $WRAPPER " service-complete --help) \
24+ || fail " supported lifecycle wrapper rejected service-complete"
25+ case " $help " in
26+ * --request-digest* --confirm-subscription* ) ;;
27+ * ) fail " service-complete help lost its exact execution binding" ;;
28+ esac
29+ pass " supported lifecycle wrapper exposes service-complete"
30+ }
31+
32+ service_complete_replay_contract () {
33+ python3 - " $CONTROLLER " << 'PY ' || fail "service completion replay contract failed"
34+ import contextlib
35+ import importlib.util
36+ from types import SimpleNamespace
37+ import sys
38+
39+ spec = importlib.util.spec_from_file_location("lifecycle", sys.argv[1])
40+ module = importlib.util.module_from_spec(spec)
41+ spec.loader.exec_module(module)
42+
43+ bindings = {
44+ "home_binding": "1" * 64,
45+ "task": "service-task",
46+ "task_generation": "service-generation",
47+ "assignment_generation": "asg-00000001",
48+ "account_binding": "2" * 64,
49+ "worktree_binding": "3" * 64,
50+ "repository_binding": "4" * 64,
51+ "repository_generation": "repository-generation",
52+ }
53+ request_digest = "5" * 64
54+ result_digest = "6" * 64
55+ item = {
56+ **bindings,
57+ "role": "no-mistakes",
58+ "status": "assigned",
59+ "slot": 1,
60+ }
61+ worker = {
62+ "role": "no-mistakes",
63+ "queue_key": "service-task@service-generation",
64+ "assignment_generation": bindings["assignment_generation"],
65+ "bindings": bindings,
66+ "cloud_instance_id": "worker-instance",
67+ "resources": {"vm": {"id": "/exact/vm"}},
68+ "last_execution_digest": result_digest,
69+ "release_proof": None,
70+ }
71+ execution = {
72+ "request_digest": request_digest,
73+ "result_digest": result_digest,
74+ "assignment_generation": bindings["assignment_generation"],
75+ }
76+ state = {
77+ "queue": {"service-task@service-generation": item},
78+ "workers": {"1": worker},
79+ "executions": {request_digest: execution},
80+ }
81+ module.controller_lock = lambda _env: contextlib.nullcontext()
82+ module.load_state = lambda _env: state
83+ module.save_state = lambda _env, _state: None
84+ args = SimpleNamespace(
85+ task="service-task",
86+ task_generation="service-generation",
87+ assignment_generation=bindings["assignment_generation"],
88+ request_digest=request_digest,
89+ confirm_subscription="subscription",
90+ )
91+ env = {"subscription": "subscription"}
92+
93+ # Crash window one: the first call durably moved the item to releasing, but
94+ # the caller died before observing success. The exact retry is idempotent.
95+ module.command_service_complete(env, args)
96+ assert item["status"] == "releasing", item
97+ assert item["service_completion_receipt"] == worker["release_proof"], item
98+ module.command_service_complete(env, args)
99+
100+ # Crash window two: reconcile completed the reset and removed the worker, but
101+ # the caller died before publishing the cached result. The queue-owned exact
102+ # receipt survives reset and admits only the same bound completion request.
103+ item["status"] = "complete"
104+ # The released slot may already belong to a later task. Its presence cannot
105+ # invalidate the old queue item's exact, self-digested completion receipt.
106+ state["workers"] = {"1": {
107+ "queue_key": "later-task@later-generation",
108+ "role": "author",
109+ "assignment_generation": "asg-00000002",
110+ "release_proof": None,
111+ }}
112+ module.command_service_complete(env, args)
113+ wrong = SimpleNamespace(**vars(args))
114+ wrong.assignment_generation = "asg-99999999"
115+ try:
116+ module.command_service_complete(env, wrong)
117+ except module.LifecycleError as exc:
118+ assert "identity differs" in str(exc), exc
119+ else:
120+ raise AssertionError("completed service receipt admitted a foreign assignment")
121+
122+ # The provider-side regression below emits this exact terminal shape. Prove
123+ # the lifecycle consumer recognizes it as terminal (rather than malformed),
124+ # fails closed, and leaves the durable execute claim available for explicit
125+ # abandonment/recovery.
126+ terminal_action = {
127+ "type": "execute",
128+ "slot": 1,
129+ "request_digest": "7" * 64,
130+ "idempotency_key": "8" * 64,
131+ "resources": {"task-command": {"id": "/exact/task-command"}},
132+ }
133+ terminal_state = {
134+ "queue": {"terminal-task@terminal-generation": {"status": "assigned"}},
135+ "workers": {"1": {"queue_key": "terminal-task@terminal-generation"}},
136+ "executions": {},
137+ "pending_actions": {"1": terminal_action},
138+ "completed_worker_seconds": 0.0,
139+ }
140+ state = terminal_state
141+ terminal_result = {"execution": {
142+ "schema": "fm.worker-execution-terminal/v1",
143+ "request_digest": terminal_action["request_digest"],
144+ "idempotency_key": terminal_action["idempotency_key"],
145+ "disposition": "provider-terminal",
146+ "provisioning_state": "Succeeded",
147+ "execution_state": "Failed",
148+ "exit_code": 2,
149+ "task_command_id": "/exact/task-command",
150+ }}
151+ try:
152+ module.apply_pending(env, terminal_action, terminal_result)
153+ except module.LifecycleError as exc:
154+ assert "provider-terminal" in str(exc) and "failed" in str(exc), exc
155+ else:
156+ raise AssertionError("failed guest execution was applied as a successful result")
157+ assert terminal_state["pending_actions"]["1"] == terminal_action, terminal_state
158+ PY
159+ pass " service completion replays across releasing and completed crash windows"
160+ }
161+
19162static_contract () {
20163 python3 - " $CONTROLLER " " $AZURE " " $SUPERVISOR " " $AUTHORITY " " $DOC " << 'PY ' || fail "elastic worker static contract failed"
21164from pathlib import Path
@@ -859,6 +1002,23 @@ recovered_kind, recovered_execution = module.execute_terminal_disposition(
8591002)
8601003assert recovered_kind == module.EXECUTE_DISPOSITION_RECOVERED, recovered_kind
8611004assert recovered_execution == execution, recovered_execution
1005+ module.run_command_instance_view = lambda *_args, **_kwargs: {
1006+ "executionState": "Failed", "exitCode": 2, "output": "", "error": "guest failed",
1007+ }
1008+ failed_kind, failed_execution = module.execute_terminal_disposition(
1009+ controller, execute_action, worker["resources"]
1010+ )
1011+ assert failed_kind == module.EXECUTE_DISPOSITION_TERMINAL, failed_kind
1012+ assert failed_execution == {
1013+ "schema": "fm.worker-execution-terminal/v1",
1014+ "request_digest": execute_action["request_digest"],
1015+ "idempotency_key": execute_action["idempotency_key"],
1016+ "disposition": "provider-terminal",
1017+ "provisioning_state": "Succeeded",
1018+ "execution_state": "Failed",
1019+ "exit_code": 2,
1020+ "task_command_id": task_command["id"],
1021+ }, failed_execution
8621022
8631023# Once the exact request owns the Run Command, a replay may only recover its
8641024# terminal disposition or exact result. Updating/Running and a Succeeded
86928852}
86938853
86948854static_contract
8855+ service_complete_front_door
8856+ service_complete_replay_contract
86958857compartment_payload_contract
86968858classification_and_admission_matrix
86978859azure_provider_refusal_matrix
0 commit comments