import { describe, expect, it } from "@effect/vitest";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as PlatformError from "effect/PlatformError";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import { TestClock } from "effect/testing";import { ChildProcessSpawner } from "effect/unstable/process";import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as ProcessRunner from "./processRunner.ts"; type ChildProcessCommand = { readonly command: string; readonly args: ReadonlyArray<string>; readonly options: { readonly shell?: boolean | string; };}; // Accesses private properties of ChildProcessCommand for testing purposesfunction asChildProcessCommand(command: unknown): ChildProcessCommand { return command as ChildProcessCommand;} function makeHandle(input: { readonly stdout?: string | Stream.Stream<Uint8Array>; readonly stderr?: string | Stream.Stream<Uint8Array>; readonly code?: number; readonly stdin?: ChildProcessSpawner.ChildProcessHandle["stdin"]; readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode>;}) { return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: input.exitCode ?? Effect.succeed(ChildProcessSpawner.ExitCode(input.code ?? 0)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: input.stdin ?? Sink.drain, stdout: typeof input.stdout === "string" ? Stream.encodeText(Stream.make(input.stdout)) : (input.stdout ?? Stream.empty), stderr: typeof input.stderr === "string" ? Stream.encodeText(Stream.make(input.stderr)) : (input.stderr ?? Stream.empty), all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, });} function makeSpawner( f: ( command: ChildProcessCommand, ) => Effect.Effect<ChildProcessSpawner.ChildProcessHandle, PlatformError.PlatformError>,) { return ChildProcessSpawner.make((command) => f(asChildProcessCommand(command)));} const runWith = (spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) => (input: ProcessRunner.ProcessRunInput) => Effect.service(ProcessRunner.ProcessRunner).pipe( Effect.flatMap((runner) => runner.run({ ...input, }), ), Effect.provide( ProcessRunner.layer.pipe( Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)), ), ), ); describe("runProcess", () => { it.effect("collects stdout through an injected ChildProcessSpawner", () => Effect.gen(function* () { const spawner = makeSpawner((command) => Effect.sync(() => { expect(command.command).toBe("fake"); expect(command.args).toEqual(["stdout-bytes", "32"]); return makeHandle({ stdout: "x".repeat(32) }); }), ); const result = yield* runWith(spawner)({ command: "fake", args: ["stdout-bytes", "32"], }); expect(result.code).toBe(0); expect(result.stdout).toBe("x".repeat(32)); expect(result.timedOut).toBe(false); }), ); it.effect("runs through the ProcessRunner service", () => { const spawner = makeSpawner((command) => Effect.sync(() => { expect(command.command).toBe("fake"); expect(command.args).toEqual(["--service"]); return makeHandle({ stdout: "service ok" }); }), ); const layer = ProcessRunner.layer.pipe( Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)), ); return Effect.gen(function* () { const runner = yield* ProcessRunner.ProcessRunner; const result = yield* runner.run({ command: "fake", args: ["--service"], }); expect(result.stdout).toBe("service ok"); }).pipe(Effect.provide(layer)); }); it.effect("resolves and escapes Windows command shims before spawning", () => { const spawner = makeSpawner((command) => Effect.sync(() => { expect(command.command).toBe('^"C:\\Users\\tester\\AppData\\Roaming\\npm\\az.cmd^"'); expect(command.args).toEqual([ '^"repos^"', '^"pr^"', '^"list^"', '^"--source-branch^"', '^"feature^ ^&^ release^"', ]); expect(command.options.shell).toBe(true); return makeHandle({ stdout: "[]" }); }), ); return runWith(spawner)({ command: "az", args: ["repos", "pr", "list", "--source-branch", "feature & release"], env: { AZURE_CONFIG_DIR: "C:\\Users\\tester\\.azure" }, }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(HostProcessEnvironment, { PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", PATHEXT: ".COM;.EXE;.BAT;.CMD", }), Effect.provideService(SpawnExecutableResolution, (_command, _platform, env) => env.PATH === "C:\\Users\\tester\\AppData\\Roaming\\npm" && env.AZURE_CONFIG_DIR === "C:\\Users\\tester\\.azure" ? "C:\\Users\\tester\\AppData\\Roaming\\npm\\az.cmd" : undefined, ), Effect.map((result) => { expect(result.stdout).toBe("[]"); }), ); }); it.effect("preserves resolved spawn context and cause", () => Effect.gen(function* () { const cause = PlatformError.systemError({ _tag: "PermissionDenied", module: "ChildProcessSpawner", method: "spawn", pathOrDescriptor: "/actual/fake", }); const spawner = makeSpawner(() => Effect.fail(cause)); const error = yield* runWith(spawner)({ command: "fake", args: ["--flag", "secret-token-value"], cwd: "/logical", spawnCwd: "/actual", }).pipe(Effect.flip); expect(error._tag).toBe("ProcessSpawnError"); if (error._tag !== "ProcessSpawnError") { return expect.fail("Expected ProcessSpawnError"); } expect(error).toMatchObject({ command: "fake", argumentCount: 2, cwd: "/logical", spawnCwd: "/actual", resolvedCommand: "fake", resolvedArgumentCount: 2, shell: false, }); expect(error.cause).toBe(cause); expect(error.message).toBe("Failed to spawn process 'fake' in '/actual'"); expect(error).not.toHaveProperty("args"); expect(error).not.toHaveProperty("resolvedArgs"); expect(error.message).not.toContain("secret-token-value"); }), ); it.effect("fails when output exceeds max buffer in default mode", () => Effect.gen(function* () { const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stdout: "x".repeat(2048) }))); const error = yield* runWith(spawner)({ command: "fake", args: ["stdout-bytes", "2048"], maxOutputBytes: 128, }).pipe(Effect.flip); expect(error._tag).toBe("ProcessOutputLimitError"); if (error._tag !== "ProcessOutputLimitError") { return expect.fail("Expected ProcessOutputLimitError"); } expect(error).toMatchObject({ stream: "stdout", maxBytes: 128, observedBytes: 2048, }); expect(error.message).toBe( "Process 'fake' stdout produced 2048 bytes, exceeding the 128 byte limit", ); }), ); it.effect("accepts output at the byte limit followed by an empty chunk", () => Effect.gen(function* () { const output = new TextEncoder().encode("exactly"); const spawner = makeSpawner(() => Effect.succeed( makeHandle({ stdout: Stream.make(output, new Uint8Array()), }), ), ); const result = yield* runWith(spawner)({ command: "fake", args: ["exact-limit"], maxOutputBytes: output.byteLength, }); expect(result.stdout).toBe("exactly"); }), ); it.effect("fails fast on output limit before timeout for long-running output", () => Effect.gen(function* () { const textChunk = "x".repeat(64); const spawner = makeSpawner(() => Effect.succeed( makeHandle({ stdout: Stream.fromIterable(Array.from({ length: 10 }, () => textChunk)).pipe( Stream.encodeText, ), exitCode: Effect.never, }), ), ); const error = yield* runWith(spawner)({ command: "fake", args: ["spam-stdout"], maxOutputBytes: 128, timeout: "2 seconds", }).pipe(Effect.flip); expect(error).toBeInstanceOf(ProcessRunner.ProcessOutputLimitError); }), ); it.effect("truncates output when outputMode is truncate", () => Effect.gen(function* () { const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stdout: "x".repeat(2048) }))); const result = yield* runWith(spawner)({ command: "fake", args: ["stdout-bytes", "2048"], maxOutputBytes: 128, outputMode: "truncate", }); expect(result.code).toBe(0); expect(result.stdout.length).toBeLessThanOrEqual(128); expect(result.stdoutTruncated).toBe(true); expect(result.stderrTruncated).toBe(false); }), ); it.effect("writes stdin before waiting for exit", () => Effect.gen(function* () { const stdinWritten = yield* Deferred.make<void>(); const decoder = new TextDecoder(); const spawner = makeSpawner(() => Effect.succeed( makeHandle({ stdout: "stdin payload", stdin: Sink.forEach((chunk: Uint8Array) => { const text = decoder.decode(chunk, { stream: true }); return text.includes("stdin payload") ? Deferred.succeed(stdinWritten, undefined) : Effect.void; }), exitCode: Deferred.await(stdinWritten).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))), }), ), ); const result = yield* runWith(spawner)({ command: "fake", args: ["stdin-echo"], stdin: "stdin payload", }); expect(result.stdout).toBe("stdin payload"); expect(result.code).toBe(0); }), ); it.effect("returns output for non-zero exit codes", () => Effect.gen(function* () { const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stderr: "boom", code: 2 }))); const result = yield* runWith(spawner)({ command: "fake", args: ["stderr-exit", "boom", "2"], }); expect(result.code).toBe(2); expect(result.stderr).toBe("boom"); }), ); it.effect("fails on timeout", () => Effect.gen(function* () { const spawner = makeSpawner(() => Effect.succeed( makeHandle({ exitCode: Effect.never, }), ), ); const errorFiber = yield* runWith(spawner)({ command: "fake", args: ["sleep"], cwd: "/logical", spawnCwd: "/actual", timeout: "50 millis", }).pipe(Effect.flip, Effect.forkScoped); yield* Effect.yieldNow; yield* TestClock.adjust(Duration.millis(50)); const error = yield* Fiber.join(errorFiber); expect(error._tag).toBe("ProcessTimeoutError"); if (error._tag !== "ProcessTimeoutError") { return expect.fail("Expected ProcessTimeoutError"); } expect(error).toMatchObject({ command: "fake", argumentCount: 1, cwd: "/logical", spawnCwd: "/actual", timeoutMs: 50, }); expect(error.message).toBe("Process 'fake' in '/actual' timed out after 50ms"); }), ); it.effect("returns a synthetic timed out result when timeoutBehavior is timedOutResult", () => Effect.gen(function* () { const spawner = makeSpawner(() => Effect.succeed( makeHandle({ exitCode: Effect.never, }), ), ); const resultFiber = yield* runWith(spawner)({ command: "fake", args: ["sleep"], timeout: "50 millis", timeoutBehavior: "timedOutResult", }).pipe(Effect.forkScoped); yield* Effect.yieldNow; yield* TestClock.adjust(Duration.millis(50)); const result = yield* Fiber.join(resultFiber); expect(result).toMatchObject({ stdout: "", stderr: "", code: null, timedOut: true, stdoutTruncated: false, stderrTruncated: false, }); }), );}); describe("isWindowsCommandNotFound", () => { it.effect("matches the localized German cmd.exe error text", () => Effect.gen(function* () { const isCommandNotFound = yield* ProcessRunner.isWindowsCommandNotFound( 1, "wird nicht als interner oder externer Befehl, betriebsfahiges Programm oder Batch-Datei erkannt", ).pipe(Effect.provideService(HostProcessPlatform, "win32")); expect(isCommandNotFound).toBe(true); }), );});
import { describe, expect, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import { TestClock } from "effect/testing";
import { ChildProcessSpawner } from "effect/unstable/process";
import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { SpawnExecutableResolution } from "@t3tools/shared/shell";
import * as ProcessRunner from "./processRunner.ts";
type ChildProcessCommand = {
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly options: {
readonly shell?: boolean | string;
};
};
// Accesses private properties of ChildProcessCommand for testing purposes
function asChildProcessCommand(command: unknown): ChildProcessCommand {
return command as ChildProcessCommand;
}
function makeHandle(input: {
readonly stdout?: string | Stream.Stream<Uint8Array>;
readonly stderr?: string | Stream.Stream<Uint8Array>;
readonly code?: number;
readonly stdin?: ChildProcessSpawner.ChildProcessHandle["stdin"];
readonly exitCode?: Effect.Effect<ChildProcessSpawner.ExitCode>;
}) {
return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode: input.exitCode ?? Effect.succeed(ChildProcessSpawner.ExitCode(input.code ?? 0)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
stdin: input.stdin ?? Sink.drain,
stdout:
typeof input.stdout === "string"
? Stream.encodeText(Stream.make(input.stdout))
: (input.stdout ?? Stream.empty),
stderr:
typeof input.stderr === "string"
? Stream.encodeText(Stream.make(input.stderr))
: (input.stderr ?? Stream.empty),
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
});
}
function makeSpawner(
f: (
command: ChildProcessCommand,
) => Effect.Effect<ChildProcessSpawner.ChildProcessHandle, PlatformError.PlatformError>,
) {
return ChildProcessSpawner.make((command) => f(asChildProcessCommand(command)));
}
const runWith =
(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) =>
(input: ProcessRunner.ProcessRunInput) =>
Effect.service(ProcessRunner.ProcessRunner).pipe(
Effect.flatMap((runner) =>
runner.run({
...input,
}),
),
Effect.provide(
ProcessRunner.layer.pipe(
Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)),
),
),
);
describe("runProcess", () => {
it.effect("collects stdout through an injected ChildProcessSpawner", () =>
Effect.gen(function* () {
const spawner = makeSpawner((command) =>
Effect.sync(() => {
expect(command.command).toBe("fake");
expect(command.args).toEqual(["stdout-bytes", "32"]);
return makeHandle({ stdout: "x".repeat(32) });
}),
);
const result = yield* runWith(spawner)({
command: "fake",
args: ["stdout-bytes", "32"],
});
expect(result.code).toBe(0);
expect(result.stdout).toBe("x".repeat(32));
expect(result.timedOut).toBe(false);
}),
);
it.effect("runs through the ProcessRunner service", () => {
const spawner = makeSpawner((command) =>
Effect.sync(() => {
expect(command.command).toBe("fake");
expect(command.args).toEqual(["--service"]);
return makeHandle({ stdout: "service ok" });
}),
);
const layer = ProcessRunner.layer.pipe(
Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)),
);
return Effect.gen(function* () {
const runner = yield* ProcessRunner.ProcessRunner;
const result = yield* runner.run({
command: "fake",
args: ["--service"],
});
expect(result.stdout).toBe("service ok");
}).pipe(Effect.provide(layer));
});
it.effect("resolves and escapes Windows command shims before spawning", () => {
const spawner = makeSpawner((command) =>
Effect.sync(() => {
expect(command.command).toBe('^"C:\\Users\\tester\\AppData\\Roaming\\npm\\az.cmd^"');
expect(command.args).toEqual([
'^"repos^"',
'^"pr^"',
'^"list^"',
'^"--source-branch^"',
'^"feature^ ^&^ release^"',
]);
expect(command.options.shell).toBe(true);
return makeHandle({ stdout: "[]" });
}),
);
return runWith(spawner)({
command: "az",
args: ["repos", "pr", "list", "--source-branch", "feature & release"],
env: { AZURE_CONFIG_DIR: "C:\\Users\\tester\\.azure" },
}).pipe(
Effect.provideService(HostProcessPlatform, "win32"),
Effect.provideService(HostProcessEnvironment, {
PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm",
PATHEXT: ".COM;.EXE;.BAT;.CMD",
}),
Effect.provideService(SpawnExecutableResolution, (_command, _platform, env) =>
env.PATH === "C:\\Users\\tester\\AppData\\Roaming\\npm" &&
env.AZURE_CONFIG_DIR === "C:\\Users\\tester\\.azure"
? "C:\\Users\\tester\\AppData\\Roaming\\npm\\az.cmd"
: undefined,
),
Effect.map((result) => {
expect(result.stdout).toBe("[]");
}),
);
});
it.effect("preserves resolved spawn context and cause", () =>
Effect.gen(function* () {
const cause = PlatformError.systemError({
_tag: "PermissionDenied",
module: "ChildProcessSpawner",
method: "spawn",
pathOrDescriptor: "/actual/fake",
});
const spawner = makeSpawner(() => Effect.fail(cause));
const error = yield* runWith(spawner)({
command: "fake",
args: ["--flag", "secret-token-value"],
cwd: "/logical",
spawnCwd: "/actual",
}).pipe(Effect.flip);
expect(error._tag).toBe("ProcessSpawnError");
if (error._tag !== "ProcessSpawnError") {
return expect.fail("Expected ProcessSpawnError");
}
expect(error).toMatchObject({
command: "fake",
argumentCount: 2,
cwd: "/logical",
spawnCwd: "/actual",
resolvedCommand: "fake",
resolvedArgumentCount: 2,
shell: false,
});
expect(error.cause).toBe(cause);
expect(error.message).toBe("Failed to spawn process 'fake' in '/actual'");
expect(error).not.toHaveProperty("args");
expect(error).not.toHaveProperty("resolvedArgs");
expect(error.message).not.toContain("secret-token-value");
}),
);
it.effect("fails when output exceeds max buffer in default mode", () =>
Effect.gen(function* () {
const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stdout: "x".repeat(2048) })));
const error = yield* runWith(spawner)({
command: "fake",
args: ["stdout-bytes", "2048"],
maxOutputBytes: 128,
}).pipe(Effect.flip);
expect(error._tag).toBe("ProcessOutputLimitError");
if (error._tag !== "ProcessOutputLimitError") {
return expect.fail("Expected ProcessOutputLimitError");
}
expect(error).toMatchObject({
stream: "stdout",
maxBytes: 128,
observedBytes: 2048,
});
expect(error.message).toBe(
"Process 'fake' stdout produced 2048 bytes, exceeding the 128 byte limit",
);
}),
);
it.effect("accepts output at the byte limit followed by an empty chunk", () =>
Effect.gen(function* () {
const output = new TextEncoder().encode("exactly");
const spawner = makeSpawner(() =>
Effect.succeed(
makeHandle({
stdout: Stream.make(output, new Uint8Array()),
}),
),
);
const result = yield* runWith(spawner)({
command: "fake",
args: ["exact-limit"],
maxOutputBytes: output.byteLength,
});
expect(result.stdout).toBe("exactly");
}),
);
it.effect("fails fast on output limit before timeout for long-running output", () =>
Effect.gen(function* () {
const textChunk = "x".repeat(64);
const spawner = makeSpawner(() =>
Effect.succeed(
makeHandle({
stdout: Stream.fromIterable(Array.from({ length: 10 }, () => textChunk)).pipe(
Stream.encodeText,
),
exitCode: Effect.never,
}),
),
);
const error = yield* runWith(spawner)({
command: "fake",
args: ["spam-stdout"],
maxOutputBytes: 128,
timeout: "2 seconds",
}).pipe(Effect.flip);
expect(error).toBeInstanceOf(ProcessRunner.ProcessOutputLimitError);
}),
);
it.effect("truncates output when outputMode is truncate", () =>
Effect.gen(function* () {
const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stdout: "x".repeat(2048) })));
const result = yield* runWith(spawner)({
command: "fake",
args: ["stdout-bytes", "2048"],
maxOutputBytes: 128,
outputMode: "truncate",
});
expect(result.code).toBe(0);
expect(result.stdout.length).toBeLessThanOrEqual(128);
expect(result.stdoutTruncated).toBe(true);
expect(result.stderrTruncated).toBe(false);
}),
);
it.effect("writes stdin before waiting for exit", () =>
Effect.gen(function* () {
const stdinWritten = yield* Deferred.make<void>();
const decoder = new TextDecoder();
const spawner = makeSpawner(() =>
Effect.succeed(
makeHandle({
stdout: "stdin payload",
stdin: Sink.forEach((chunk: Uint8Array) => {
const text = decoder.decode(chunk, { stream: true });
return text.includes("stdin payload")
? Deferred.succeed(stdinWritten, undefined)
: Effect.void;
}),
exitCode: Deferred.await(stdinWritten).pipe(Effect.as(ChildProcessSpawner.ExitCode(0))),
}),
),
);
const result = yield* runWith(spawner)({
command: "fake",
args: ["stdin-echo"],
stdin: "stdin payload",
});
expect(result.stdout).toBe("stdin payload");
expect(result.code).toBe(0);
}),
);
it.effect("returns output for non-zero exit codes", () =>
Effect.gen(function* () {
const spawner = makeSpawner(() => Effect.succeed(makeHandle({ stderr: "boom", code: 2 })));
const result = yield* runWith(spawner)({
command: "fake",
args: ["stderr-exit", "boom", "2"],
});
expect(result.code).toBe(2);
expect(result.stderr).toBe("boom");
}),
);
it.effect("fails on timeout", () =>
Effect.gen(function* () {
const spawner = makeSpawner(() =>
Effect.succeed(
makeHandle({
exitCode: Effect.never,
}),
),
);
const errorFiber = yield* runWith(spawner)({
command: "fake",
args: ["sleep"],
cwd: "/logical",
spawnCwd: "/actual",
timeout: "50 millis",
}).pipe(Effect.flip, Effect.forkScoped);
yield* Effect.yieldNow;
yield* TestClock.adjust(Duration.millis(50));
const error = yield* Fiber.join(errorFiber);
expect(error._tag).toBe("ProcessTimeoutError");
if (error._tag !== "ProcessTimeoutError") {
return expect.fail("Expected ProcessTimeoutError");
}
expect(error).toMatchObject({
command: "fake",
argumentCount: 1,
cwd: "/logical",
spawnCwd: "/actual",
timeoutMs: 50,
});
expect(error.message).toBe("Process 'fake' in '/actual' timed out after 50ms");
}),
);
it.effect("returns a synthetic timed out result when timeoutBehavior is timedOutResult", () =>
Effect.gen(function* () {
const spawner = makeSpawner(() =>
Effect.succeed(
makeHandle({
exitCode: Effect.never,
}),
),
);
const resultFiber = yield* runWith(spawner)({
command: "fake",
args: ["sleep"],
timeout: "50 millis",
timeoutBehavior: "timedOutResult",
}).pipe(Effect.forkScoped);
yield* Effect.yieldNow;
yield* TestClock.adjust(Duration.millis(50));
const result = yield* Fiber.join(resultFiber);
expect(result).toMatchObject({
stdout: "",
stderr: "",
code: null,
timedOut: true,
stdoutTruncated: false,
stderrTruncated: false,
});
}),
);
});
describe("isWindowsCommandNotFound", () => {
it.effect("matches the localized German cmd.exe error text", () =>
Effect.gen(function* () {
const isCommandNotFound = yield* ProcessRunner.isWindowsCommandNotFound(
1,
"wird nicht als interner oder externer Befehl, betriebsfahiges Programm oder Batch-Datei erkannt",
).pipe(Effect.provideService(HostProcessPlatform, "win32"));
expect(isCommandNotFound).toBe(true);
}),
);
});