1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { GrokSettings } from "@t3tools/provider-grok/settings";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
8import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
9import * as HostProcess from "@t3tools/shared/HostProcess";
10import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
11
12import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
13import { GROK_ACP_CANCEL_META, GROK_ACP_INITIALIZE_META } from "@t3tools/provider-grok/testing";
14import { makeXAiPromptCompletionRuntime } from "@t3tools/provider-grok/testing";
15import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
16import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
17import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";
18import type { ProviderReplayGate } from "@t3tools/provider-testing/replayGate";
19import type { OrchestratorV2ProviderReplayHarness } from "../testkit/ProviderReplayHarness.ts";
20import {
21 type AcpReplayTranscript,
22 AcpReplayTranscriptDecodeError,
23 decodeAcpReplayTranscript,
24 makeAcpReplayCompletenessAssertion,
25 makeAcpReplayRuntime,
26} from "./AcpAdapterV2.testkit.ts";
27import {
28 GROK_DEFAULT_INSTANCE_ID,
29 GROK_PROVIDER,
30 makeGrokAdapterV2,
31} from "@t3tools/provider-grok/testing";
32
33const DEFAULT_GROK_SETTINGS = Schema.decodeUnknownSync(GrokSettings)({});
34
35function layerGrokProviderAdapterRegistryReplay(
36 transcript: AcpReplayTranscript,
37 options: { readonly replayGate?: ProviderReplayGate } = {},
38) {
39 const layerHost = TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer));
40
41 return ProviderAdapterRegistry.layerFromAdaptersEffect(
42 Effect.gen(function* () {
43 const fileSystem = yield* FileSystem.FileSystem;
44 const path = yield* Path.Path;
45 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
46 const hostPlatform = yield* HostProcess.Platform;
47 // Same queue the continuation worker drains when the fixture runs it.
48 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
49 const replayGate = options.replayGate;
50 const replayDir = yield* fileSystem
51 .makeTempDirectory({
52 prefix: `t3-orchestration-v2-grok-replay-${transcript.scenario}-`,
53 })
54 .pipe(Effect.orDie);
55 const statusPath = path.join(replayDir, "status.json");
56 const scriptPath = yield* path
57 .fromFileUrl(new URL("../../../scripts/acp-replay-agent.ts", import.meta.url))
58 .pipe(Effect.orDie);
59 const adapter = yield* makeGrokAdapterV2({
60 instanceId: GROK_DEFAULT_INSTANCE_ID,
61 settings: DEFAULT_GROK_SETTINGS,
62 environment: {},
63 hostPlatform,
64 selfInvocation: yield* resolveSelfInvocation(),
65 // Same wrapping as makeGrokAcpRuntime: client type and Ctrl+C cancel
66 // metadata and the x.ai prompt-completion race, so replay sends what
67 // production sends.
68 makeRuntime: (runtimeInput) =>
69 makeAcpReplayRuntime({
70 transcript,
71 statusPath,
72 scriptPath,
73 childProcessSpawner,
74 fileSystem,
75 ...(replayGate === undefined ? {} : { replayGate }),
76 cancelMeta: GROK_ACP_CANCEL_META,
77 initializeMeta: GROK_ACP_INITIALIZE_META,
78 })(runtimeInput).pipe(Effect.flatMap(makeXAiPromptCompletionRuntime)),
79 continuationRequests,
80 assertComplete: makeAcpReplayCompletenessAssertion(fileSystem, statusPath, transcript),
81 ...(replayGate === undefined
82 ? {}
83 : {
84 testHooks: {
85 onDeferredFinalizeScheduled: (debounce) =>
86 Effect.sync(() => replayGate.recordFinishArmed(debounce)),
87 },
88 }),
89 });
90 return [adapter];
91 }),
92 ).pipe(
93 Layer.provide(Layer.mergeAll(layerHost, NodeServices.layer, IdAllocator.layer)),
94 // Held inbound lines must not outlive the scenario and wedge teardown.
95 Layer.merge(
96 Layer.effectDiscard(
97 Effect.addFinalizer(() => Effect.sync(() => options.replayGate?.releaseAll())),
98 ),
99 ),
100 );
101}
102
103export const GrokOrchestratorReplayHarness: OrchestratorV2ProviderReplayHarness<
104 AcpReplayTranscript,
105 AcpReplayTranscriptDecodeError
106> = {
107 driver: GROK_PROVIDER,
108 decodeTranscript: (transcript) => decodeAcpReplayTranscript(transcript, GROK_PROVIDER),
109 makeProviderAdapterRegistryLayer: layerGrokProviderAdapterRegistryReplay,
110};