Skip to content

Commit 6d0ae5b

Browse files
committed
Merge remote-tracking branch 'origin/main' into rust-325-train-oct22c
2 parents db7254b + b57e3c9 commit 6d0ae5b

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

‎app/Http/Controllers/Api/HistoryController.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ public function export(Request $request, string $workflowId, string $runId): Jso
139139
$fresh = $run->fresh() ?? $run;
140140
$bundle = $repository->runHistoryExport($fresh);
141141

142-
return ControlPlaneProtocol::json(LegacyV1Projection::decorateHistoryExport($bundle, $fresh));
142+
return ControlPlaneProtocol::json(
143+
LegacyV1Projection::decorateHistoryExport($bundle, $fresh),
144+
options: JSON_PRESERVE_ZERO_FRACTION,
145+
);
143146
}
144147

145148
private function loadEvents(string $runId, ?int $afterSequence, int $pageSize)

‎app/Support/ControlPlaneProtocol.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ public static function jsonForRequest(Request $request, array $payload, int $sta
7272
return self::json($payload, $status);
7373
}
7474

75-
public static function json(array $payload, int $status = 200): JsonResponse
75+
public static function json(array $payload, int $status = 200, int $options = 0): JsonResponse
7676
{
7777
return response()
78-
->json($payload, $status)
78+
->json($payload, $status, [], $options)
7979
->header(self::HEADER, self::VERSION);
8080
}
8181

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Illuminate\Foundation\Testing\RefreshDatabase;
6+
use Illuminate\Support\Facades\Queue;
7+
use Illuminate\Support\Str;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Tests\Feature\Concerns\ServerTestHelpers;
10+
use Tests\Fixtures\ExternalGreetingWorkflow;
11+
use Tests\TestCase;
12+
use Workflow\V2\Enums\TaskStatus;
13+
use Workflow\V2\Enums\TaskType;
14+
use Workflow\V2\Jobs\RunActivityTask;
15+
use Workflow\V2\Models\WorkflowTask;
16+
use Workflow\V2\Support\BundleIntegrityVerifier;
17+
use Workflow\V2\Support\MemoPayload;
18+
19+
class HistoryExportNumericKindsTest extends TestCase
20+
{
21+
use RefreshDatabase;
22+
use ServerTestHelpers;
23+
24+
public static function runStates(): iterable
25+
{
26+
yield 'waiting' => [false];
27+
yield 'completed' => [true];
28+
}
29+
30+
#[DataProvider('runStates')]
31+
public function test_http_export_preserves_numeric_kinds(bool $complete): void
32+
{
33+
Queue::fake();
34+
$this->createNamespace('default');
35+
$this->createNamespace('other');
36+
config(['workflows.v2.types.workflows' => [
37+
'tests.external-greeting-workflow' => ExternalGreetingWorkflow::class,
38+
]]);
39+
$memo = [
40+
'nested' => ['list' => [3.0, -0.0, 1.5, 4], 'signed_zero' => -0.0, 'whole' => 2.0],
41+
'signed_zero' => -0.0,
42+
'whole' => 1.0,
43+
];
44+
$id = 'numeric-export-'.Str::ulid();
45+
$start = $this->postJson('/api/workflows', [
46+
'workflow_id' => $id,
47+
'workflow_type' => 'tests.external-greeting-workflow',
48+
'input' => ['Ada'],
49+
'memo' => $memo,
50+
'run_timeout_seconds' => 30,
51+
'execution_timeout_seconds' => 30,
52+
], $this->apiHeaders(), JSON_PRESERVE_ZERO_FRACTION);
53+
$start->assertCreated();
54+
$runId = (string) $start->json('run_id');
55+
$this->runReadyWorkflowTask($runId);
56+
if ($complete) {
57+
$task = WorkflowTask::query()->where('workflow_run_id', $runId)
58+
->where('status', TaskStatus::Ready->value)
59+
->where('task_type', TaskType::Activity->value)->sole();
60+
$this->app->call([new RunActivityTask($task->id), 'handle']);
61+
$this->runReadyWorkflowTask($runId);
62+
}
63+
64+
$route = "/api/workflows/{$id}/runs/{$runId}/history/export";
65+
$response = $this->withHeaders($this->apiHeaders())->getJson($route);
66+
$response->assertOk()
67+
->assertHeader('X-Durable-Workflow-Control-Plane-Version', '2')
68+
->assertJsonMissingPath('control_plane')
69+
->assertJsonPath('workflow.status', $complete ? 'completed' : 'waiting');
70+
$raw = $response->getContent();
71+
$bundle = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
72+
$projection = $bundle['workflow']['memo'];
73+
$this->assertSame($memo, $projection);
74+
$this->assertSame(-INF, fdiv(1.0, $projection['signed_zero']));
75+
$this->assertSame(-INF, fdiv(1.0, $projection['nested']['signed_zero']));
76+
$this->assertSame(-INF, fdiv(1.0, $projection['nested']['list'][1]));
77+
$this->assertSame($memo, MemoPayload::decodeEntries($bundle['workflow']['memo_payload']));
78+
$report = BundleIntegrityVerifier::verifyJson($raw);
79+
$this->assertSame(BundleIntegrityVerifier::STATUS_OK, $report['status'], json_encode($report, JSON_THROW_ON_ERROR));
80+
81+
$path = sys_get_temp_dir().'/server-numeric-export-'.Str::ulid().'.json';
82+
file_put_contents($path, $raw);
83+
try {
84+
$hash = hash_file('sha256', $path);
85+
$this->artisan('workflow:v2:replay-verify', [
86+
'bundle' => $path, '--skip-replay' => true, '--json' => true,
87+
])->assertSuccessful();
88+
$this->assertSame($hash, hash_file('sha256', $path));
89+
} finally {
90+
unlink($path);
91+
}
92+
$lossy = BundleIntegrityVerifier::verifyJson(json_encode($bundle, JSON_THROW_ON_ERROR));
93+
$this->assertSame(BundleIntegrityVerifier::STATUS_FAILED, $lossy['status']);
94+
$this->assertContains('workflow.memo_projection_mismatch', array_column($lossy['findings'], 'rule'));
95+
$this->assertContains('integrity.checksum_mismatch', array_column($lossy['findings'], 'rule'));
96+
97+
$this->withHeaders($this->apiHeaders('other'))->getJson($route)->assertNotFound();
98+
}
99+
}

0 commit comments

Comments
 (0)