Files
agents-marketplace/gateway/test/ocr.test.ts
FilipBl4gojevic 7d348e762f
Some checks failed
CI / typecheck + vitest (Node 20) (push) Has been cancelled
fix(ocr): live busy pre-check before posting a candidate's escrow
runOcrOneShot posted the escrow before calling the supplier, so a busy
supplier's 409 stranded an already-paid escrow until reclaim - and the
buyer-side busy-retry multiplies this into several stranded escrows per
page. Add a live /status check per candidate, immediately before its
escrow posts, mirroring sessions.ts's startChat pre-check: "working"
skips to the next candidate without posting, anything else (free,
unreachable, malformed, slow) is treated as usable so the check is an
optimisation, never a gate. Bounded to a 2s timeout so a stalled
supplier cannot meaningfully delay the request.

Existing candidate-fallback, escrow-ref capture, usage recording, and
receipt labels are unchanged.
2026-08-10 19:31:22 +02:00

573 lines
23 KiB
TypeScript

/**
* gateway/test/ocr.test.ts — POST /v1/ocr/extract route coverage.
*
* Focus: auth gating, demo-key rejection, body validation, capability-based
* supplier lookup, and the enlarged body limit. The settle path (submitOcr →
* resolveSubmittedRef → acceptAndConfirm) reuses the one-shot chat plumbing
* covered by http.test.ts + the SDK suites; end-to-end runs live on testnet.
*/
import { describe, it, expect, vi } from "vitest";
import { tmpdir } from "os";
import { join } from "path";
import { randomBytes, randomUUID } from "crypto";
import request from "supertest";
import { createApp } from "../src/server.js";
import { GatewayStore } from "../src/db/store.js";
import { SdkRegistry } from "../src/sdk/registry.js";
import { genPrivKeyHex, deriveWalletKey } from "../src/wallet.js";
import { seal } from "../src/crypto/seal.js";
import { hashApiKey } from "../src/middleware/apiKeyAuth.js";
import { venueAttribution } from "../src/ocr/extract.js";
import type { GatewayConfig } from "../src/config.js";
import type { GatewayDeps } from "../src/deps.js";
import { SupplierError } from "@marketplace/buyer/sdk";
// The one-shot settle step (resolveSubmittedRef → acceptAndConfirm) talks to
// the live indexer + chain; the happy-path route test below only needs to
// prove that a successful submitOcr reaches the response with the
// first-party-brokered disclosure label (I3), so it stubs just those two
// chain-touching steps and keeps every other export real.
vi.mock("../src/onchain/settle.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/onchain/settle.js")>();
return {
...actual,
resolveSubmittedRef: vi.fn(async () => ({ txHash: "e".repeat(64), index: 0 })),
acceptAndConfirm: vi.fn(async () => "f".repeat(64)),
};
});
const MASTER = "ab".repeat(32);
const IMG = Buffer.from("page bytes").toString("base64");
/** A funded wallet: covers price+bonds+collateral+fee and has pure-ADA ≥ 5. */
const fundedChain = {
queryUtxosByAddress: async () => [
{ ref: { txHash: "ff".repeat(32), index: 0 }, address: "a", lovelace: 20_000_000n, assets: {}, datumHex: null, scriptRef: null },
],
} as unknown as GatewayDeps["chain"];
const OCR_SUPPLIER = {
utxo_ref: `${"cc".repeat(32)}#0`,
supplier_pkh: "s3",
capability_id: "ocr.page.extract.chandra-ocr-2.v1",
model: "chandra-ocr-2",
max_output_tokens: 4096,
price_lovelace: "200000",
supplier_bond_lovelace: "1000000",
buyer_bond_lovelace: "1000000",
endpoint_url: "http://sup",
advert_status: "Active",
status: "free",
};
function makeDeps(fetchFn?: typeof globalThis.fetch, overrides?: { chain?: GatewayDeps["chain"] }): GatewayDeps {
const dbDir = join(tmpdir(), `gw-ocr-test-${randomUUID()}`);
const store = new GatewayStore(dbDir);
const config: GatewayConfig = {
masterKeyHex: MASTER,
indexerUrl: "http://ix",
ogmiosUrl: "http://og",
networkId: 0,
liveChain: true,
port: 0,
dbDir,
signupRate: { max: 1000, windowMs: 60_000 },
keyRate: { max: 1000, windowMs: 60_000 },
demoIpRate: { max: 1000, windowMs: 60_000 },
sweeperIntervalMs: 60_000,
demoSessionIdleMs: 180_000,
chatSettleMode: "full",
walletHealthIntervalMs: 600_000,
sdkRegistryMax: 100,
ocrCapabilityId: "ocr.page.extract.chandra-ocr-2.v1",
corsOrigins: [],
};
const chain = overrides?.chain ?? ({ queryUtxosByAddress: async () => [] } as unknown as GatewayDeps["chain"]);
const registry = new SdkRegistry({
chain,
indexerUrl: config.indexerUrl,
networkId: config.networkId,
masterKeyHex: config.masterKeyHex,
max: config.sdkRegistryMax,
});
const defaultFetch = (async () => new Response("[]", { status: 200 })) as unknown as typeof globalThis.fetch;
return { config, store, chain, registry, fetchFn: fetchFn ?? defaultFetch };
}
/** Insert a key row (demo or funded) and return the raw bearer token. */
function makeKey(deps: GatewayDeps, demo: 0 | 1): string {
const rawKey = `vk-${randomBytes(24).toString("hex")}`;
const privHex = genPrivKeyHex();
const walletKey = deriveWalletKey(privHex, 0);
const sealed = seal(privHex, MASTER);
deps.store.insertKey({
id: randomUUID(),
key_hash: hashApiKey(rawKey),
key_prefix: rawKey.slice(0, 12),
label: demo ? "demo" : "funded",
wallet_pkh: walletKey.pubKeyHash,
deposit_address: walletKey.address,
enc_priv_nonce: sealed.nonce,
enc_priv_ct: sealed.ct,
enc_priv_tag: sealed.tag,
master_key_version: 1,
created_at: Date.now(),
demo,
});
return rawKey;
}
function validBody() {
return { image_b64: IMG, mime: "image/png", output_format: "markdown" };
}
describe("POST /v1/ocr/extract — auth + demo gating", () => {
it("401 without a bearer key, OpenAI error shape", async () => {
const res = await request(createApp(makeDeps()))
.post("/v1/ocr/extract").send(validBody());
expect(res.status).toBe(401);
expect(res.body.error?.type).toBe("authentication_error");
});
it("403 demo_key_unsupported for demo keys", async () => {
const deps = makeDeps();
const rawKey = makeKey(deps, 1);
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(res.status).toBe(403);
expect(res.body.error?.code).toBe("demo_key_unsupported");
});
});
describe("POST /v1/ocr/extract — body validation", () => {
async function post(body: unknown) {
const deps = makeDeps();
const rawKey = makeKey(deps, 0);
return request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(body as object);
}
it("400 invalid_image on missing/empty/data-URL image_b64", async () => {
for (const image_b64 of [undefined, "", `data:image/png;base64,${IMG}`]) {
const res = await post({ ...validBody(), image_b64 });
expect(res.status).toBe(400);
expect(res.body.error?.code).toBe("invalid_image");
}
});
it("400 invalid_mime on unsupported mime", async () => {
const res = await post({ ...validBody(), mime: "image/gif" });
expect(res.status).toBe(400);
expect(res.body.error?.code).toBe("invalid_mime");
});
it("400 invalid_output_format on unsupported format", async () => {
const res = await post({ ...validBody(), output_format: "yaml" });
expect(res.status).toBe(400);
expect(res.body.error?.code).toBe("invalid_output_format");
});
it("output_format defaults to markdown (reaches supplier lookup)", async () => {
const res = await post({ image_b64: IMG, mime: "image/png" });
// Passes validation, then the empty indexer answer → 404 model_not_found.
expect(res.status).toBe(404);
expect(res.body.error?.code).toBe("model_not_found");
});
});
describe("POST /v1/ocr/extract — supplier lookup + body limit", () => {
it("404 model_not_found when no supplier advertises the capability", async () => {
const deps = makeDeps();
const rawKey = makeKey(deps, 0);
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(res.status).toBe(404);
expect(res.body.error?.code).toBe("model_not_found");
});
it("queries the indexer with the configured ocr capability id", async () => {
let queried = "";
const fetchFn = (async (url: unknown) => {
queried = String(url);
return new Response("[]", { status: 200 });
}) as unknown as typeof globalThis.fetch;
const deps = makeDeps(fetchFn);
const rawKey = makeKey(deps, 0);
await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(queried).toContain("capability_id=ocr.page.extract.chandra-ocr-2.v1");
});
it("accepts a 2MB body (over the 1mb global limit — OCR route has its own parser)", async () => {
const deps = makeDeps();
const rawKey = makeKey(deps, 0);
const big = "A".repeat(2_000_000);
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send({ ...validBody(), image_b64: big });
expect(res.status).not.toBe(413);
// Valid base64 + no suppliers → 404 model_not_found.
expect(res.status).toBe(404);
});
it("chat completions keep the 1mb ceiling (413 on a 2MB body)", async () => {
const deps = makeDeps();
const rawKey = makeKey(deps, 0);
const big = "A".repeat(2_000_000);
const res = await request(createApp(deps))
.post("/openai/v1/chat/completions")
.set("Authorization", `Bearer ${rawKey}`)
.send({ model: "qwen", messages: [{ role: "user", content: big }] });
expect(res.status).toBe(413);
});
});
// ─── venueAttribution — pure-function header sanitization (I3) ──────────────
//
// grep for "first-party-brokered" used to return only source lines: nothing
// exercised venueAttribution's regex gates or RECEIPT_LABELS_V0 reaching the
// wire. These two blocks close that gap.
function fakeReq(headers: Record<string, string>): Parameters<typeof venueAttribution>[0] {
return { header: (name: string) => headers[name] } as unknown as Parameters<typeof venueAttribution>[0];
}
describe("venueAttribution — header sanitization", () => {
it("passes through a valid venue slug and a valid lowercase hash", () => {
const hash = "a".repeat(64);
const out = venueAttribution(fakeReq({ "X-Venue": "apify", "X-Run-User-Hash": hash }));
expect(out).toEqual({ venue: "apify", runUserHash: hash });
});
it("lowercases a valid but uppercase hex hash", () => {
const out = venueAttribution(fakeReq({ "X-Venue": "apify", "X-Run-User-Hash": "A".repeat(64) }));
expect(out.runUserHash).toBe("a".repeat(64));
});
it("drops an invalid venue to empty string (uppercase, bad chars, oversized)", () => {
for (const bad of ["Apify", "apify!", "apify_venue", "a".repeat(33)]) {
const out = venueAttribution(fakeReq({ "X-Venue": bad, "X-Run-User-Hash": "a".repeat(64) }));
expect(out.venue).toBe("");
}
});
it("drops an invalid run-user-hash to empty string (wrong length, non-hex)", () => {
for (const bad of ["a".repeat(63), "a".repeat(65), "z".repeat(64), "not-hex-at-all"]) {
const out = venueAttribution(fakeReq({ "X-Venue": "apify", "X-Run-User-Hash": bad }));
expect(out.runUserHash).toBe("");
}
});
it("defaults both fields to empty string when headers are missing", () => {
const out = venueAttribution(fakeReq({}));
expect(out).toEqual({ venue: "", runUserHash: "" });
});
});
// ─── Happy path — receipt disclosure label reaches the response (I3) ────────
describe("POST /v1/ocr/extract — happy path", () => {
it("200 response x_vector.receipt_labels contains first-party-brokered", async () => {
const fetchFn = (async (url: unknown) => {
const u = String(url);
if (u.includes("/suppliers")) {
return new Response(JSON.stringify([OCR_SUPPLIER]), { status: 200 });
}
return new Response("[]", { status: 200 });
}) as unknown as typeof globalThis.fetch;
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
// Stub only the chain-touching supplier call; escrow ref shape matches
// what resolveSubmittedRef's mock (module-level, top of file) expects.
const ctx = deps.registry.getContext(keyRow);
const submitOcr = vi.fn(async () => ({
output_format: "markdown",
content: "extracted text",
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
receipt: {
prompt_hash: "1b".repeat(32),
response_hash: "2c".repeat(32),
model: "chandra-ocr-2",
prompt_tokens: 10,
completion_tokens: 5,
wallclock_ms: Date.now(),
supplier_pkh: "s3",
escrow_ref: `${"d".repeat(64)}#0`,
},
receiptSignature: "sig".repeat(20),
escrowRef: { txHash: "d".repeat(64), index: 0 },
}));
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(res.status).toBe(200);
expect(submitOcr).toHaveBeenCalledTimes(1);
expect(res.body.x_vector.receipt_labels).toContain("first-party-brokered");
// escrow_ref in the response comes from submitOcr's returned escrowRef
// (the ORIGINAL Open ref), not the mocked resolveSubmittedRef's answer —
// see settle.ts's docstring on why those two refs differ on-chain.
expect(res.body.x_vector.escrow_ref).toBe(`${"d".repeat(64)}#0`);
});
});
// ─── Failure path — escrow ref survives a post-escrow SupplierError (I2) ────
//
// submitOcr posts the escrow, THEN calls the supplier. A SupplierError
// (timeout, network failure, non-2xx, malformed response, receipt mismatch)
// means the escrow already landed on chain even though submitOcr rejects —
// per the OCR route's 120s supplier budget, this is likely the MOST common
// paid-but-failed shape. Before this fix, escrowRefStr was only assigned
// from submitOcr's *return value*, so this exact case recorded escrow_ref:
// null. The route now also listens for the SDK's "escrow_posted" progress
// event around each attempt, so the ref survives regardless of whether
// submitOcr goes on to resolve or reject.
describe("POST /v1/ocr/extract — escrow ref survives a post-escrow SupplierError", () => {
it("records the escrow_ref from the escrow_posted progress event, not null, when submitOcr posts an escrow then rejects", async () => {
const fetchFn = (async (url: unknown) => {
const u = String(url);
if (u.includes("/suppliers")) {
return new Response(JSON.stringify([OCR_SUPPLIER]), { status: 200 });
}
return new Response("[]", { status: 200 });
}) as unknown as typeof globalThis.fetch;
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
// Same monkeypatch pattern as the happy-path test above: fetch the real
// per-key Marketplace instance from the registry BEFORE the request so
// its real EventEmitter (on/off/emit) stays live, then replace only
// submitOcr. The stub mirrors what Marketplace.submitOcr actually does
// on this failure shape: emit "escrow_posted" the instant the tx
// confirms, then throw once the supplier call fails.
const ctx = deps.registry.getContext(keyRow);
const knownEscrowRef = `${"9".repeat(64)}#0`;
const submitOcr = vi.fn(async () => {
ctx.sdk.emit("progress", { type: "escrow_posted", escrow_ref: knownEscrowRef });
throw new SupplierError("timeout", { message: "supplier did not respond within budget" });
});
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
// Snapshot the recorded calls BEFORE restoring — mockRestore() also
// clears .mock.calls (it runs mockReset()/mockClear() first), so
// reading logSpy.mock.calls after restore always sees [].
const loggedLines = logSpy.mock.calls.map((args) => String(args[0]));
logSpy.mockRestore();
expect(submitOcr).toHaveBeenCalledTimes(1);
// toGatewayError maps SupplierError("timeout") to 504 supplier_timeout —
// confirms the request actually took the failure path this test targets.
expect(res.status).toBe(504);
expect(res.body.error?.code).toBe("supplier_timeout");
// The reconciliation spine: the usage row recorded in the catch block
// must carry the real ref, not null.
const rows = deps.store.listUsage(keyRow.id, 10);
expect(rows).toHaveLength(1);
expect(rows[0].status).toBe("failed");
expect(rows[0].escrow_ref).toBe(knownEscrowRef);
// The structured ocr_failed log line (wedge-ledger emitter input) must
// carry the same ref.
const failedLine = loggedLines
.filter((line) => line.includes('"evt":"ocr_failed"'))
.map((line) => JSON.parse(line) as { escrow_ref: string | null });
expect(failedLine).toHaveLength(1);
expect(failedLine[0].escrow_ref).toBe(knownEscrowRef);
});
});
// ─── Busy pre-check - never post an escrow to a candidate that is busy ──────
//
// runOcrOneShot now checks each candidate's live /status immediately before
// calling submitOcr for it (mirrors sessions.ts's startChat pre-check):
// "working" means busy and moves on to the next candidate WITHOUT posting an
// escrow; anything else (free, unreachable, malformed, slow) is treated as
// usable. submitOcr is the escrow-posting path in these tests (same
// monkeypatch pattern as the suites above), so asserting its call count is
// exactly how "no escrow was posted" is observed.
const OCR_SUPPLIER_A = {
...OCR_SUPPLIER,
utxo_ref: `${"aa".repeat(32)}#0`,
supplier_pkh: "sA",
endpoint_url: "http://sup-a",
};
const OCR_SUPPLIER_B = {
...OCR_SUPPLIER,
utxo_ref: `${"bb".repeat(32)}#0`,
supplier_pkh: "sB",
endpoint_url: "http://sup-b",
};
/** Indexer /suppliers lookup plus a per-endpoint /status responder, on one
* fetchFn - everything else falls back to the suite's usual empty array. */
function makeFetchWithStatus(
suppliers: Array<typeof OCR_SUPPLIER>,
statusByEndpoint: Record<string, () => Response>,
): typeof globalThis.fetch {
return (async (url: unknown) => {
const u = String(url);
if (u.includes("/suppliers")) {
return new Response(JSON.stringify(suppliers), { status: 200 });
}
for (const [endpoint, respond] of Object.entries(statusByEndpoint)) {
if (u === `${endpoint}/status`) return respond();
}
return new Response("[]", { status: 200 });
}) as unknown as typeof globalThis.fetch;
}
/** Same successful submitOcr shape as the happy-path suite above, reused
* across the busy pre-check tests. */
function makeSubmitOcrStub() {
return vi.fn(async (_opts: { advertRef: { txHash: string; index: number } }) => ({
output_format: "markdown",
content: "extracted text",
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
receipt: {
prompt_hash: "1b".repeat(32),
response_hash: "2c".repeat(32),
model: "chandra-ocr-2",
prompt_tokens: 10,
completion_tokens: 5,
wallclock_ms: Date.now(),
supplier_pkh: "s3",
escrow_ref: `${"d".repeat(64)}#0`,
},
receiptSignature: "sig".repeat(20),
escrowRef: { txHash: "d".repeat(64), index: 0 },
}));
}
describe("POST /v1/ocr/extract - busy pre-check", () => {
it("a busy supplier is never sent an escrow; the route returns the busy response", async () => {
const fetchFn = makeFetchWithStatus([OCR_SUPPLIER], {
"http://sup": () => new Response(JSON.stringify({ status: "working" }), { status: 200 }),
});
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
const ctx = deps.registry.getContext(keyRow);
const submitOcr = makeSubmitOcrStub();
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
// The escrow-posting path was never invoked.
expect(submitOcr).not.toHaveBeenCalled();
// toGatewayError maps SupplierError("supplier_busy") to 503 overloaded -
// the same busy response a post-escrow 409 would have produced.
expect(res.status).toBe(503);
expect(res.body.error?.code).toBe("overloaded");
const rows = deps.store.listUsage(keyRow.id, 10);
expect(rows).toHaveLength(1);
expect(rows[0].status).toBe("failed");
expect(rows[0].escrow_ref).toBeNull();
});
it("a free supplier proceeds to post and settle exactly as before", async () => {
const fetchFn = makeFetchWithStatus([OCR_SUPPLIER], {
"http://sup": () => new Response(JSON.stringify({ status: "free" }), { status: 200 }),
});
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
const ctx = deps.registry.getContext(keyRow);
const submitOcr = makeSubmitOcrStub();
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(submitOcr).toHaveBeenCalledTimes(1);
expect(res.status).toBe(200);
expect(res.body.x_vector.receipt_labels).toContain("first-party-brokered");
});
it("falls back to the second candidate when the first is busy, posting exactly one escrow", async () => {
const fetchFn = makeFetchWithStatus([OCR_SUPPLIER_A, OCR_SUPPLIER_B], {
"http://sup-a": () => new Response(JSON.stringify({ status: "working" }), { status: 200 }),
"http://sup-b": () => new Response(JSON.stringify({ status: "free" }), { status: 200 }),
});
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
const ctx = deps.registry.getContext(keyRow);
const submitOcr = makeSubmitOcrStub();
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
expect(res.status).toBe(200);
// Exactly one escrow posted, and it is the second (free) candidate's -
// the busy first candidate was skipped, not retried after failing.
expect(submitOcr).toHaveBeenCalledTimes(1);
expect(submitOcr.mock.calls[0][0].advertRef).toEqual({ txHash: "bb".repeat(32), index: 0 });
});
it("a status check that errors does not block the attempt", async () => {
const fetchFn = makeFetchWithStatus([OCR_SUPPLIER], {
"http://sup": () => {
// Simulates both a network error and an aborted (timed-out) request -
// isSupplierBusy's catch block treats every failure mode the same
// way: unknown, not busy, proceed. A real AbortSignal timeout is not
// exercised here to keep the suite fast; the outcome is identical.
throw new Error("simulated network failure reaching /status");
},
});
const deps = makeDeps(fetchFn, { chain: fundedChain });
const rawKey = makeKey(deps, 0);
const keyRow = deps.store.getKeyByHash(hashApiKey(rawKey))!;
const ctx = deps.registry.getContext(keyRow);
const submitOcr = makeSubmitOcrStub();
(ctx.sdk as { submitOcr: unknown }).submitOcr = submitOcr;
const res = await request(createApp(deps))
.post("/v1/ocr/extract")
.set("Authorization", `Bearer ${rawKey}`)
.send(validBody());
// The candidate was still tried despite the failed pre-check.
expect(submitOcr).toHaveBeenCalledTimes(1);
expect(res.status).toBe(200);
});
});