@selesai/code 0.13.35 → 0.13.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  * assert what the agent observes — the state Jev was sent, the answers that come
7
7
  * back, and what never leaves the process — never private helper order.
8
8
  */
9
- import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -32,8 +32,26 @@ vi.mock("@selesai/code", () => ({
32
32
  }),
33
33
  }));
34
34
 
35
- import jevAskToolExtension, { askPath, FIND_BATCH, fitFindPayload, MAX_ASK_QUESTIONS } from "./jev-ask-tool.ts";
36
- import { JEV_ROUTING_EVENT, serializeJevRequest } from "./jev/decisions.ts";
35
+ import jevAskToolExtension, {
36
+ askPath,
37
+ childDirectory,
38
+ FIND_BATCH,
39
+ FIND_MAX_REQUESTS,
40
+ FIND_MAX_SOURCE_BYTES,
41
+ FindRequests,
42
+ fitJudgePayloads,
43
+ MAX_ASK_QUESTIONS,
44
+ renderSourceBlocks,
45
+ runJevFind,
46
+ unitView,
47
+ } from "./jev-ask-tool.ts";
48
+ import { chunkUnits, loadTypeScript, splitSourceUnits } from "./jev-find-source.ts";
49
+ import {
50
+ JEV_ROUTING_EVENT,
51
+ type JevAbstainReason,
52
+ type JevFailureDiagnostic,
53
+ serializeJevRequest,
54
+ } from "./jev/decisions.ts";
37
55
  import { jevResponse, providerTemplate } from "./jev/test-support.ts";
38
56
 
39
57
  const FILE_SENTINEL = "FILE_BODY_MUST_RETURN_TO_JE ONLY";
@@ -425,14 +443,346 @@ describe("jev_find", () => {
425
443
  return { text: result.content[0]?.text ?? "", details: result.details };
426
444
  };
427
445
 
446
+ type Stage = "directories" | "files" | "units";
447
+ /**
448
+ * A fake Jev for `runJevFind`: it reads each request the way Jev would (state keyed by stage,
449
+ * one noul per quoted key) and answers with `decide`, refusing an oversized request exactly as
450
+ * the real transport does. `fail` scripts whole-request failures by stage and attempt.
451
+ */
452
+ function fakeJev(
453
+ decide: (stage: Stage, key: string) => number | undefined,
454
+ options: {
455
+ maxBytes?: number;
456
+ fail?: (stage: Stage, attempt: number) => JevAbstainReason | undefined;
457
+ diagnostic?: JevFailureDiagnostic;
458
+ } = {},
459
+ ) {
460
+ const calls: Array<{ stage: Stage; keys: string[]; bytes: number; payload: Record<string, unknown> }> = [];
461
+ const ask = async (payload: Record<string, unknown>) => {
462
+ const state = payload.state as Record<string, unknown>;
463
+ const stage = (["directories", "files", "units"] as const).find((key) => key in state) as Stage;
464
+ const questions = payload.questions as Record<string, { instructions: { question: string } }>;
465
+ const keys = Object.values(questions).map((q) => /"([^"]+)"/.exec(q.instructions.question)?.[1] ?? "");
466
+ const bytes = Buffer.byteLength(JSON.stringify(payload), "utf-8");
467
+ calls.push({ stage, keys, bytes, payload });
468
+ if (serializeJevRequest(payload, options.maxBytes ?? 32 * 1024) === undefined) {
469
+ return { failure: "overflow" as const, elapsedMs: 0 };
470
+ }
471
+ const failure = options.fail?.(stage, calls.filter((call) => call.stage === stage).length);
472
+ if (failure) return { failure, ...(options.diagnostic ? { diagnostic: options.diagnostic } : {}), elapsedMs: 1 };
473
+ const answers: Record<string, unknown> = {};
474
+ Object.keys(questions).forEach((name, index) => {
475
+ const noul = decide(stage, keys[index]);
476
+ if (noul !== undefined) answers[name] = { noul };
477
+ });
478
+ return { answers, elapsedMs: 1 };
479
+ };
480
+ return { ask, calls, keys: (stage: Stage) => calls.filter((call) => call.stage === stage).flatMap((call) => call.keys) };
481
+ }
482
+
483
+ let tree: string;
484
+ const RETRY_SOURCE = [
485
+ 'import { x } from "./x";',
486
+ "",
487
+ "/** Doubles the delay on every retry. */",
488
+ "export function retryBackoff(attempt: number) {",
489
+ "\treturn 100 * 2 ** attempt;",
490
+ "}",
491
+ "",
492
+ "export const unrelated = x; // not the retry path",
493
+ "",
494
+ ].join("\n");
495
+
428
496
  beforeAll(() => {
429
497
  writeFileSync(join(repo, "src", "other.ts"), "// mentions round only in passing\nexport const x = 1;\n", "utf-8");
430
498
  writeFileSync(join(repo, ".env"), "round=SECRET_ENV_VALUE\n", "utf-8");
499
+ // 86 files: alpha/ (30), beta/core/ (30), beta/extra/ (25), and README.md at the root.
500
+ tree = join(root, "tree");
501
+ for (const [dir, count] of [["alpha", 30], ["beta/core", 30], ["beta/extra", 25]] as const) {
502
+ mkdirSync(join(tree, dir), { recursive: true });
503
+ for (let i = 0; i < count; i += 1) writeFileSync(join(tree, dir, `f${i}.ts`), `export const v${i} = ${i};\n`, "utf-8");
504
+ }
505
+ writeFileSync(join(tree, "beta/core/f3.ts"), RETRY_SOURCE, "utf-8");
506
+ writeFileSync(join(tree, "README.md"), "# Tree\n\nA fixture.\n", "utf-8");
507
+ });
508
+
509
+ const QUESTION = "how is the retry backoff computed";
510
+ const TREE_VERDICTS = (stage: Stage, key: string): number | undefined => {
511
+ if (stage === "directories") return ({ "alpha/": 0.1, "beta/": 0.9, "beta/core/": 0.8, "beta/extra/": 0.2 } as Record<string, number>)[key];
512
+ if (stage === "files") return key === "beta/core/f3.ts" ? 0.95 : 0.05;
513
+ return key.includes("retryBackoff") ? 0.9 : 0.1;
514
+ };
515
+
516
+ describe("directory descent", () => {
517
+ it("prunes a rejected directory and recurses into a kept one, judging root files alongside", async () => {
518
+ const jev = fakeJev(TREE_VERDICTS);
519
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
520
+
521
+ expect(jev.keys("directories")).toEqual(["beta/", "alpha/", "beta/core/", "beta/extra/"]);
522
+ // The kept directory is described by its most promising files and keyword hits.
523
+ const betaState = (jev.calls[0].payload.state as { directories: Record<string, string> }).directories["beta/"];
524
+ expect(betaState).toMatch(/^55 file\(s\): core\/f3\.ts/);
525
+ expect(betaState).toContain("Keyword hits:\ncore/f3.ts L3: /** Doubles the delay on every retry. */");
526
+ const judgedFiles = jev.keys("files");
527
+ expect(judgedFiles).toHaveLength(31);
528
+ expect(judgedFiles).toContain("README.md");
529
+ expect(judgedFiles.some((path) => path.startsWith("alpha/") || path.startsWith("beta/extra/"))).toBe(false);
530
+
531
+ expect(result.text).toMatch(/^jev_find: 1 relevant file\(s\) \(judged 31 of 86 candidates via jev-test in \d+ms\)$/m);
532
+ expect(result.text).toContain("Jev ruled out 2 directories holding 55 file(s).");
533
+ expect(result.text).toContain("- beta/core/f3.ts (0.95) — reading leads: retryBackoff lines 3-6");
534
+ expect(result.text.trimEnd().endsWith("End context.")).toBe(true);
535
+ expect(result.details).toMatchObject({ total: 86, judged: 31, relevant: 1 });
536
+ });
537
+
538
+ it("keeps directories whose question failed, as unknown rather than rejected", async () => {
539
+ const jev = fakeJev(TREE_VERDICTS, { fail: (stage) => (stage === "directories" ? "timeout" : undefined) });
540
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
541
+
542
+ const judgedFiles = jev.keys("files");
543
+ expect(judgedFiles).toHaveLength(86);
544
+ expect(judgedFiles).toContain("alpha/f0.ts");
545
+ expect(result.text).not.toContain("ruled out");
546
+ expect(result.text).toMatch(/directories were kept unjudged \(timeout\)/);
547
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:');
548
+ });
549
+
550
+ it("stops free descent at the directory depth limit", async () => {
551
+ const deepTree = join(root, "deep-tree");
552
+ for (const branch of ["one", "two"]) {
553
+ const dir = join(deepTree, "a", "b", "c", "d", "e", branch);
554
+ mkdirSync(dir, { recursive: true });
555
+ for (let i = 0; i < 30; i += 1) writeFileSync(join(dir, `backoff${i}.ts`), `export const backoff${i} = ${i};\n`, "utf-8");
556
+ }
557
+ const jev = fakeJev(() => 0.9);
558
+ const result = await runJevFind({
559
+ rg: "rg",
560
+ cwd: deepTree,
561
+ root: ".",
562
+ question: "backoff",
563
+ maxBytes: 32 * 1024,
564
+ model: "jev-test",
565
+ ask: jev.ask,
566
+ });
567
+
568
+ expect(jev.keys("directories")).toEqual([]);
569
+ expect(jev.keys("files")).toHaveLength(60);
570
+ expect(result.details.requests).toBeLessThanOrEqual(FIND_MAX_REQUESTS);
571
+ });
572
+
573
+ it("names the next directory level below a root", () => {
574
+ expect(childDirectory("src/a/b.ts", ".")).toBe("src");
575
+ expect(childDirectory("src/a/b.ts", "src")).toBe("src/a");
576
+ expect(childDirectory("src/b.ts", "src")).toBeUndefined();
577
+ });
578
+ });
579
+
580
+ describe("source units", () => {
581
+ it("splits TypeScript into statements, merged imports, and class members that keep their header", async () => {
582
+ const ts = await loadTypeScript();
583
+ expect(ts).toBeDefined();
584
+ const filler = Array.from({ length: 40 }, (_, i) => `\t\tconst step${i} = ${i};`);
585
+ const text = [
586
+ 'import { a } from "a";', // 1
587
+ 'import { b } from "b";', // 2
588
+ "", // 3
589
+ "// ---- section banner ----", // 4
590
+ "", // 5
591
+ "/** Adds one. */", // 6
592
+ "export function add(x: number) {", // 7
593
+ "\treturn x + a + b;", // 8
594
+ "}", // 9
595
+ "", // 10
596
+ "export class Big {", // 11
597
+ "\t/** The first step. */", // 12
598
+ "\tfirst() {", // 13
599
+ ...filler, // 14-53
600
+ "\t}", // 54
601
+ "\tsecond() { return 2; } // trailing note", // 55
602
+ "}", // 56
603
+ "",
604
+ ].join("\n");
605
+ expect(splitSourceUnits("x.ts", text, ts)).toEqual([
606
+ { name: "imports", start: 1, end: 2 },
607
+ { name: "add", start: 6, end: 9 },
608
+ { name: "Big.first", start: 12, end: 54, header: 11 },
609
+ { name: "Big.second", start: 55, end: 55, header: 11 },
610
+ ]);
611
+ });
612
+
613
+ it("chunks other files, and scripts when TypeScript is missing, by blank lines into at most 40 lines", () => {
614
+ const long = Array.from({ length: 45 }, (_, i) => `line ${i}`);
615
+ const text = ["# Title", "intro", "", "## Setup", "run it", "", ...long, ""].join("\n");
616
+ expect(splitSourceUnits("README.md", text, undefined)).toEqual([
617
+ { name: "# Title", start: 1, end: 5 },
618
+ { name: "line 0", start: 7, end: 46 },
619
+ { name: "line 40", start: 47, end: 51 },
620
+ ]);
621
+ expect(splitSourceUnits("x.ts", RETRY_SOURCE, undefined)).toEqual(chunkUnits(RETRY_SOURCE));
622
+ });
623
+
624
+ it("returns accepted units verbatim with their own line numbers", async () => {
625
+ const jev = fakeJev(TREE_VERDICTS);
626
+ const result = await runJevFind({
627
+ rg: "rg",
628
+ cwd: tree,
629
+ root: "beta/core",
630
+ question: QUESTION,
631
+ maxBytes: 32 * 1024,
632
+ model: "jev-test",
633
+ ask: jev.ask,
634
+ });
635
+ const block = result.text.split('Source block "beta/core/f3.ts" lines 3-6:\n')[1]?.split("\n").slice(0, 4);
636
+ expect(block).toEqual(RETRY_SOURCE.split("\n").slice(2, 6).map((line, i) => `${i + 3}: ${line}`));
637
+ // Only the unit Jev accepted is returned; the rejected one is not.
638
+ expect(jev.keys("units")).toContain("beta/core/f3.ts lines 8-8 (unrelated)");
639
+ expect(result.text).not.toContain("8: export const unrelated");
640
+ });
641
+
642
+ it("falls back to the matching source unit when Jev cannot judge the units", async () => {
643
+ const jev = fakeJev(TREE_VERDICTS, { fail: (stage) => (stage === "units" ? "timeout" : undefined) });
644
+ const result = await runJevFind({ rg: "rg", cwd: tree, root: "beta/core", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
645
+
646
+ expect(result.text).toContain("- beta/core/f3.ts (0.95) — reading leads: retryBackoff lines 3-6");
647
+ expect(result.text).toContain("Jev did not judge the source units of 1 file(s) (timeout); matching source units or keyword windows are shown instead.");
648
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:\n3: /** Doubles the delay on every retry. */');
649
+ expect(result.text).not.toContain("1: import { x }");
650
+ });
651
+
652
+ it("keeps the returned source within the output budget", async () => {
653
+ const big = join(root, "big");
654
+ mkdirSync(big, { recursive: true });
655
+ const fn = (n: number) => [
656
+ `export function retry${n}() {`,
657
+ ...Array.from({ length: 58 }, (_, i) => `\tconst backoff${i} = "${"x".repeat(60)}";`),
658
+ "}",
659
+ "",
660
+ ];
661
+ writeFileSync(join(big, "big.ts"), Array.from({ length: 8 }, (_, n) => fn(n)).flat().join("\n"), "utf-8");
662
+ const jev = fakeJev(() => 0.9);
663
+ const result = await runJevFind({ rg: "rg", cwd: big, root: ".", question: QUESTION, maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
664
+
665
+ expect(result.details.sourceBytes).toBeLessThanOrEqual(FIND_MAX_SOURCE_BYTES);
666
+ expect(result.details.sourceBytes).toBeGreaterThan(FIND_MAX_SOURCE_BYTES / 2);
667
+ expect(result.text).toMatch(/source block\(s\) left out to stay within 16 KB/);
668
+ // Every numbered line is the file's own line under its own number.
669
+ const rows = readFileSync(join(big, "big.ts"), "utf-8").split("\n");
670
+ for (const match of result.text.matchAll(/^(\d+): (.*)$/gm)) expect(rows[Number(match[1]) - 1]).toBe(match[2]);
671
+ });
672
+
673
+ it("clips a block to the byte budget and leaves out one that cannot show three lines", () => {
674
+ const rows = Array.from({ length: 20 }, (_, i) => `row ${i + 1}`);
675
+ const unit = { name: "u", start: 1, end: 20 };
676
+ const rendered = renderSourceBlocks(
677
+ [
678
+ { path: "a.txt", unit, view: unitView(unit, undefined), rows },
679
+ { path: "b.txt", unit, view: unitView(unit, undefined), rows },
680
+ ],
681
+ 60,
682
+ );
683
+ expect(rendered.bytes).toBeLessThanOrEqual(60);
684
+ expect(rendered.lines).toEqual(['', 'Source block "a.txt" lines 1-20:', "1: row 1", "2: row 2", "3: row 3", "4: row 4", "5: row 5", "6: row 6", "…"]);
685
+ expect(rendered.omitted).toBe(1);
686
+ });
687
+
688
+ it("shows a long unit around its best keyword line", () => {
689
+ expect(unitView({ name: "f", start: 10, end: 200 }, 150, 20)).toEqual([10, 0, ...Array.from({ length: 19 }, (_, i) => 140 + i), 0]);
690
+ expect(unitView({ name: "m", start: 30, end: 31, header: 5 }, undefined)).toEqual([5, 0, 30, 31]);
691
+ });
431
692
  });
432
693
 
433
- it("ranks ripgrep's candidates by Jev relevance and returns pointers, never contents", async () => {
694
+ describe("reliability", () => {
695
+ it("retries a transport failure once and then judges", async () => {
696
+ const jev = fakeJev((stage, key) => (stage === "files" ? (key.endsWith("round.ts") ? 0.9 : 0.1) : 0.9), {
697
+ fail: (stage, attempt) => (stage === "files" && attempt === 1 ? "transport" : undefined),
698
+ });
699
+ const result = await runJevFind({ rg: "rg", cwd: repo, root: "src", question: "rounding", maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
700
+
701
+ expect(jev.calls.filter((call) => call.stage === "files")).toHaveLength(2);
702
+ expect(result.details).toMatchObject({ judged: 2, retried: 1 });
703
+ expect(result.text).toContain("- src/round.ts (0.90)");
704
+ });
705
+
706
+ it("retries only once, then still returns ripgrep-ranked source", async () => {
707
+ const jev = fakeJev(() => 0.9, { fail: () => "transport" });
708
+ const result = await runJevFind({ rg: "rg", cwd: repo, root: "src", question: "rounding", maxBytes: 32 * 1024, model: "jev-test", ask: jev.ask });
709
+
710
+ expect(jev.calls).toHaveLength(2);
711
+ expect(result.text).toMatch(/^jev_find: Jev did not judge \(transport\); 2 candidate file\(s\), ranked by ripgrep:/);
712
+ expect(result.text).toContain('Source block "src/round.ts" lines 1-1:');
713
+ });
714
+
715
+ it("reports sanitized provider status and returns the matching source unit on transport failure", async () => {
716
+ const jev = fakeJev(TREE_VERDICTS, {
717
+ fail: () => "transport",
718
+ diagnostic: { kind: "provider-error", httpStatus: 503 },
719
+ });
720
+ const result = await runJevFind({
721
+ rg: "rg",
722
+ cwd: tree,
723
+ root: "beta/core",
724
+ question: QUESTION,
725
+ pattern: "retry",
726
+ maxBytes: 32 * 1024,
727
+ model: "jev-test",
728
+ ask: jev.ask,
729
+ });
730
+
731
+ expect(result.text).toContain("Diagnostic: Jev provider request failed (HTTP 503).");
732
+ expect(result.details).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 503 } });
733
+ expect(result.text).toContain("retryBackoff lines 3-6");
734
+ expect(result.text).toContain('Source block "beta/core/f3.ts" lines 3-6:\n3: /** Doubles the delay on every retry. */');
735
+ });
736
+
737
+ it("never sends more than the request cap", async () => {
738
+ const asked: number[] = [];
739
+ const requests = new FindRequests(async () => {
740
+ asked.push(1);
741
+ return { failure: "transport", elapsedMs: 0 };
742
+ }, 3);
743
+ const results = await requests.sendAll([{}, {}, {}, {}, {}], 5);
744
+ expect(asked).toHaveLength(3);
745
+ expect(results.slice(3)).toEqual([undefined, undefined]);
746
+ expect(requests.left).toBe(0);
747
+ });
748
+
749
+ it("fits every payload in every stage within maxBytes (the overflow regression)", async () => {
750
+ // The old fit gave up once snippets hit 64 bytes and sent the rest oversized: the questions, paths,
751
+ // and scaffolding of a full batch alone can outgrow a small budget.
752
+ const long = `src/${"deeply/nested/".repeat(12)}file.ts`;
753
+ const items = Array.from({ length: FIND_BATCH }, (_, i) => ({ key: `${long}${i}`, text: '"\n\t\\'.repeat(500), question: `Is "${long}${i}" relevant?` }));
754
+ const stage = { request: "q", stateKey: "files", criteria: { true: "yes", false: "no" } };
755
+ const fitted = fitJudgePayloads(stage, items, 4 * 1024);
756
+ expect(fitted.dropped).toEqual([]);
757
+ expect(fitted.payloads.flatMap((entry) => entry.items).sort((a, b) => a - b)).toEqual(items.map((_, i) => i));
758
+ for (const entry of fitted.payloads) expect(serializeJevRequest(entry.payload, 4 * 1024)).toBeDefined();
759
+ // An item that cannot fit alone is dropped, never sent oversized.
760
+ expect(fitJudgePayloads(stage, [{ key: "k", text: "", question: "x".repeat(5_000) }], 4 * 1024)).toEqual({ payloads: [], dropped: [0] });
761
+
762
+ const maxBytes = 6 * 1024;
763
+ const descent = fakeJev(TREE_VERDICTS, { maxBytes });
764
+ await runJevFind({ rg: "rg", cwd: tree, root: ".", question: `${QUESTION} ${"really ".repeat(2_000)}`, maxBytes, model: "jev-test", ask: descent.ask });
765
+ const units = fakeJev(TREE_VERDICTS, { maxBytes });
766
+ await runJevFind({ rg: "rg", cwd: tree, root: "beta/core", question: QUESTION, maxBytes, model: "jev-test", ask: units.ask });
767
+ const calls = [...descent.calls, ...units.calls];
768
+ expect(new Set(calls.map((call) => call.stage))).toEqual(new Set(["directories", "files", "units"]));
769
+ for (const call of calls) expect(call.bytes).toBeLessThanOrEqual(maxBytes);
770
+ expect(descent.calls.length).toBeLessThanOrEqual(FIND_MAX_REQUESTS);
771
+ });
772
+ });
773
+
774
+ it("registers jev_find as a finder that returns source, pointing subagents at it", () => {
775
+ const tool = harness().tools.jev_find;
776
+ expect(tool.description).toContain("verbatim");
777
+ expect(tool.description).not.toMatch(/never contents|pointers only/i);
778
+ expect(tool.description).toContain(".gitignore");
779
+ expect(tool.promptSnippet).toContain("source");
780
+ expect(tool.promptGuidelines?.join("\n")).toContain("subagent");
781
+ });
782
+
783
+ it("ranks files, returns the accepted source verbatim, and never sends secret files", async () => {
434
784
  const session = harness();
435
- // Jev: the file whose question names round.ts is relevant, the rest are not.
785
+ // Jev: anything naming round.ts is relevant, the rest are not.
436
786
  session.complete.mockImplementation(async (_model: unknown, request: { messages: Array<{ content: string }> }) => {
437
787
  const sent = JSON.parse(request.messages[0].content) as { questions: Record<string, { instructions: { question: string } }> };
438
788
  const answers = Object.fromEntries(
@@ -442,33 +792,29 @@ describe("jev_find", () => {
442
792
  });
443
793
  const result = await find(session, { question: "where is rounding implemented", pattern: "round", ignoreCase: true });
444
794
 
445
- expect(result.text).toMatch(/judged 2 of 2 candidate/);
446
- expect(result.text).toContain("- src/round.ts:1 (0.93)");
447
- expect(result.text).not.toContain("other.ts");
448
- expect(result.text).not.toContain(FILE_SENTINEL);
449
- const sent = session.sent() as { state: { files: Record<string, string> } };
450
- expect(Object.keys(sent.state.files).sort()).toEqual(["src/other.ts", "src/round.ts"]);
451
- expect(sent.state.files["src/round.ts"]).toContain(FILE_SENTINEL);
452
- expect(JSON.stringify(sent)).not.toContain("SECRET_ENV_VALUE");
795
+ expect(result.text).toMatch(/^jev_find: 1 relevant file\(s\) \(judged 2 of 2 candidates via jev-1\.13 in \d+ms\)/);
796
+ // The sentinel line is not valid TypeScript; the parser recovers and still names the declaration.
797
+ expect(result.text).toMatch(/^- src\/round\.ts \(0\.93\) — reading leads: round\b.* lines 1-1$/m);
798
+ expect(result.text).toContain(`Source block "src/round.ts" lines 1-1:\n1: export const round = () => ${FILE_SENTINEL};`);
799
+ expect(result.text).not.toContain("- src/other.ts");
800
+ const everything = JSON.stringify(session.complete.mock.calls);
801
+ expect(everything).toContain("src/other.ts");
802
+ expect(everything).not.toContain("SECRET_ENV_VALUE");
803
+ expect(session.telemetry.at(-1)).toMatchObject({ route: "find", outcome: "jev" });
453
804
  });
454
805
 
455
- it("lists a directory by glob and still returns candidates when Jev is unreachable", async () => {
806
+ it("still returns ripgrep-ranked files and their source when Jev is unreachable", async () => {
456
807
  const session = harness({ credential: false });
457
808
  const result = await find(session, { question: "rounding", path: "src", glob: "*.ts" });
458
809
 
459
810
  expect(session.complete).not.toHaveBeenCalled();
460
- expect(result.text).toMatch(/Jev did not judge \(no-credential\); 2 candidate/);
811
+ const lines = result.text.split("\n");
812
+ expect(lines[0]).toBe("jev_find: Jev did not judge (no-credential); 2 candidate file(s), ranked by ripgrep:");
461
813
  // The question word in the path orders the unjudged listing.
462
- expect(result.text.split("\n")[1]).toBe("- src/round.ts");
463
- });
464
-
465
- it("shrinks escape-heavy snippets until a full batch fits the request budget", () => {
466
- // Quotes and newlines double under JSON escaping, which is what overflowed live.
467
- const snippet = '"\n\t\\'.repeat(1_000);
468
- const batch = Array.from({ length: FIND_BATCH }, (_, i) => ({ path: `src/f${i}.ts`, lines: [], snippet }));
469
- const payload = fitFindPayload("q", batch, 32 * 1024);
470
- expect(serializeJevRequest(payload, 32 * 1024)).toBeDefined();
471
- expect(Object.keys(payload.questions as object)).toHaveLength(FIND_BATCH);
814
+ expect(lines[1]).toBe("- src/round.ts — file head lines 1-1");
815
+ expect(result.text).toContain(`Source block "src/round.ts" lines 1-1:\n1: export const round = () => ${FILE_SENTINEL};`);
816
+ expect(result.text.trimEnd().endsWith("End context.")).toBe(true);
817
+ expect(session.telemetry.at(-1)).toMatchObject({ route: "find", outcome: "fallback", reason: "no-credential" });
472
818
  });
473
819
 
474
820
  it("refuses a search root outside the working directory", async () => {