From 3df310ae12ef36f2059315232f50cd715c3af687 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 1/8] test(tui): cover empty maps, limits, and name fallback --- .../util/repository-functionality.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 05dbd777..09afc31d 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -61,4 +61,74 @@ describe("repository areas", () => { expect(renamed[0].title).toBe("Session History") expect(buildGroupNamingPrompt(groups)).toContain("Group: packages/area-34") }) + + test("handles empty repositories and ignores generated-only inventories", () => { + expect(analyzeRepositoryGroups([])).toEqual([]) + expect(analyzeRepositoryGroups(["node_modules/pkg/index.js", "dist/app.js", ".git/config"])).toEqual([]) + }) + + test("keeps root files and unfamiliar layouts without losing paths", () => { + const files = [ + "README.md", + "mystery/component.custom", + "services/api/lib/start.ts", + "modules/parser/spec/check.txt", + ] + const groups = analyzeRepositoryGroups(files) + + expect(groups.flatMap((group) => group.files).sort()).toEqual([...files].sort()) + expect(groups.find((group) => group.id === ".")?.files).toEqual(["README.md"]) + expect(groups.map((group) => group.id)).toEqual( + expect.arrayContaining(["mystery", "services/api", "modules/parser"]), + ) + }) + + test.each([20, 21, 25])("preserves every file and reference with %i subgroups", (count) => { + const files = Array.from({ length: count }, (_, index) => `packages/api/src/feature-${index}/index.ts`) + const [group] = analyzeRepositoryGroups(files, files) + + expect(group.subgroups).toHaveLength(Math.min(count, 20)) + expect(group.subgroups.flatMap((subgroup) => subgroup.files).sort()).toEqual([...files].sort()) + expect(group.subgroups.flatMap((subgroup) => subgroup.referencedFiles).sort()).toEqual([...files].sort()) + if (count > 20) { + expect(group.subgroups.at(-1)?.title).toBe("Other areas") + expect(group.subgroups.at(-1)?.files).toHaveLength(count - 19) + } + }) + + test.each([30, 31, 35])("preserves every file and reference with %i groups", (count) => { + const files = Array.from({ length: count }, (_, index) => `packages/area-${index}/src/index.ts`) + const groups = analyzeRepositoryGroups(files, files) + + expect(groups).toHaveLength(Math.min(count, 30)) + expect(groups.flatMap((group) => group.files).sort()).toEqual([...files].sort()) + expect(groups.flatMap((group) => group.referencedFiles).sort()).toEqual([...files].sort()) + expect(groups.flatMap((group) => group.subgroups.flatMap((subgroup) => subgroup.files)).sort()).toEqual( + [...files].sort(), + ) + if (count > 30) { + expect(groups.at(-1)?.title).toBe("Other repository groups") + expect(groups.at(-1)?.files).toHaveLength(count - 29) + } + }) + + test.each(["", "No names available", '{"src":', '{"src": "Source",}'])( + "retains structural names for malformed naming response %j", + (response) => { + const groups = analyzeRepositoryGroups(["src/main.ts", "docs/guide.md"]) + expect(applyGroupNames(groups, response)).toEqual(groups) + }, + ) + + test("retains names for missing and non-string values without changing the original map", () => { + const groups = analyzeRepositoryGroups(["src/main.ts", "docs/guide.md", "tests/main.test.ts"]) + const original = structuredClone(groups) + const renamed = applyGroupNames(groups, '{"src": "Application", "docs": 42, "unrelated": "Unused"}') + + expect(renamed.find((group) => group.id === "src")?.title).toBe("Application") + expect(renamed.find((group) => group.id === "docs")).toEqual(groups.find((group) => group.id === "docs")) + expect(renamed.find((group) => group.id === "tests")).toEqual(groups.find((group) => group.id === "tests")) + expect(renamed).toHaveLength(groups.length) + expect(groups).toEqual(original) + }) }) From 4cfa1f710f51b3eae32f9a4f08c1a681bb7a97eb Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 2/8] test(tui): verify read filtering and grouping invariants --- .../tui/test/util/referenced-file.test.ts | 38 +++++++++++++++ .../util/repository-functionality.test.ts | 46 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/tui/test/util/referenced-file.test.ts b/packages/tui/test/util/referenced-file.test.ts index e24111d7..5a0de1e2 100644 --- a/packages/tui/test/util/referenced-file.test.ts +++ b/packages/tui/test/util/referenced-file.test.ts @@ -14,6 +14,44 @@ describe("referenced files", () => { ).toEqual(["src/index.ts", "test/index.test.ts"]) }) + test("equivalence partitioning: metadata file paths override input paths", () => { + expect( + collectReferencedFiles([ + { + type: "tool", + tool: "read", + state: { + status: "completed", + input: { filePath: "outdated.ts" }, + metadata: { display: { type: "file", path: "src/current.ts" } }, + }, + }, + { + type: "tool", + tool: "read", + state: { + status: "completed", + input: { filePath: "directory.ts" }, + metadata: { display: { type: "directory", path: "src" } }, + }, + }, + completedRead("src/fallback.ts"), + ]), + ).toEqual(["src/current.ts", "src/fallback.ts"]) + }) + + test("equivalence partitioning: ignores unreadable or unfinished tool results", () => { + expect( + collectReferencedFiles([ + { type: "tool", tool: "read", state: { status: "completed", input: { filePath: " " } } }, + { type: "tool", tool: "read", state: { status: "completed", input: { filePath: 42 } } }, + { type: "tool", tool: "read", state: { status: "error", input: { filePath: "failed.ts" } } }, + { type: "tool", tool: "write", state: { status: "completed", input: { filePath: "written.ts" } } }, + { type: "text" }, + ]), + ).toEqual([]) + }) + test("registers /group", () => { expect(referencedFileCommand.slash.name).toBe("group") }) diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 09afc31d..8ea21040 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -83,7 +83,7 @@ describe("repository areas", () => { ) }) - test.each([20, 21, 25])("preserves every file and reference with %i subgroups", (count) => { + test.each([19, 20, 21, 25])("boundary-value analysis: preserves files with %i subgroups", (count) => { const files = Array.from({ length: count }, (_, index) => `packages/api/src/feature-${index}/index.ts`) const [group] = analyzeRepositoryGroups(files, files) @@ -96,7 +96,7 @@ describe("repository areas", () => { } }) - test.each([30, 31, 35])("preserves every file and reference with %i groups", (count) => { + test.each([29, 30, 31, 35])("boundary-value analysis: preserves files with %i groups", (count) => { const files = Array.from({ length: count }, (_, index) => `packages/area-${index}/src/index.ts`) const groups = analyzeRepositoryGroups(files, files) @@ -120,6 +120,48 @@ describe("repository areas", () => { }, ) + test("metamorphic testing: duplicate and reordered path formats preserve the map", () => { + const files = ["packages/api/src/login.ts", "packages/api/test/login.test.ts", "apps/web/src/home.tsx"] + const expected = analyzeRepositoryGroups(files, ["apps/web/src/home.tsx"]) + const changed = analyzeRepositoryGroups( + ["./apps/web/src/home.tsx", "packages\\api\\test\\login.test.ts", ...files.toReversed(), files[0]], + ["apps\\web\\src\\home.tsx"], + ) + + expect(changed).toEqual(expected) + }) + + test("generated-input property: every accepted file belongs to one group and subgroup", () => { + for (const count of [1, 2, 17, 18, 19, 30, 31, 75]) { + const files = Array.from({ length: count }, (_, index) => + index % 2 ? `apps/web/test/case-${index}.test.ts` : `packages/api/src/feature-${index}/index.ts`, + ) + const groups = analyzeRepositoryGroups( + files.flatMap((file, index) => (index % 2 ? [file, file] : [`./${file}`, file])), + [files[0], files.at(-1)!], + ) + const references = [...new Set([files[0], files.at(-1)!])].sort() + + expect(groups.flatMap((group) => group.files).sort()).toEqual([...files].sort()) + expect(groups.flatMap((group) => group.subgroups.flatMap((subgroup) => subgroup.files)).sort()).toEqual( + [...files].sort(), + ) + expect(groups.flatMap((group) => group.referencedFiles).sort()).toEqual(references) + } + }) + + test("pairwise testing: a late referenced file survives prompt length limits", () => { + const files = Array.from({ length: 35 }, (_, index) => `src/session/file-${index}.ts`) + const referenced = files.at(-1)! + const [group] = analyzeRepositoryGroups(files, [referenced]) + const prompt = buildFunctionalGroupPrompt(group, [group]) + const listed = prompt.split("Representative files:\n")[1].split("\n- (", 1)[0].split("\n") + + expect(listed).toHaveLength(15) + expect(listed[0]).toBe(`- ${referenced}`) + expect(prompt).toContain(`(${files.length - 15} additional files omitted)`) + }) + test("retains names for missing and non-string values without changing the original map", () => { const groups = analyzeRepositoryGroups(["src/main.ts", "docs/guide.md", "tests/main.test.ts"]) const original = structuredClone(groups) From efc97e076af3f3e4aaf45a8bd9c8d77a196926be Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 3/8] fix(tui): prioritize session reads in map naming --- packages/tui/src/routes/session/index.tsx | 1 + packages/tui/src/util/repository-functionality.ts | 5 ++++- .../tui/test/util/repository-functionality.test.ts | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 13cc77e7..284395a1 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -503,6 +503,7 @@ export function Session() { const naming = await sdk.client.session.prompt({ sessionID: namingSession.data.id, workspace: project.workspace.current(), + agent: "learn", parts: [{ type: "text", text: buildGroupNamingPrompt(groups) }], }) const text = naming.data?.parts diff --git a/packages/tui/src/util/repository-functionality.ts b/packages/tui/src/util/repository-functionality.ts index 47d30aae..94cdf22c 100644 --- a/packages/tui/src/util/repository-functionality.ts +++ b/packages/tui/src/util/repository-functionality.ts @@ -115,7 +115,10 @@ export function buildGroupNamingPrompt(groups: readonly FunctionalGroup[]) { return [ "Give each repository group a concise, beginner-friendly functionality name.", "Return only a JSON object mapping each exact group id to a name of at most four words.", - ...groups.flatMap((group) => [`Group: ${group.id}`, ...group.files.slice(0, 5).map((file) => `- ${file}`)]), + ...groups.flatMap((group) => [ + `Group: ${group.id}`, + ...[...new Set([...group.referencedFiles, ...group.files])].slice(0, 5).map((file) => `- ${file}`), + ]), ].join("\n") } diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 8ea21040..802772d2 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -49,6 +49,16 @@ describe("repository areas", () => { expect(prompt).not.toContain("src/session/file-8.ts") }) + test("includes session-referenced files in the naming sample", () => { + const files = Array.from({ length: 12 }, (_, index) => `src/session/file-${index}.ts`) + const referenced = files.at(-1)! + const [group] = analyzeRepositoryGroups(files, [referenced]) + const prompt = buildGroupNamingPrompt([group]) + + expect(prompt).toContain(`- ${referenced}`) + expect(prompt.split("\n- ")).toHaveLength(6) + }) + test("limits the map and applies model-generated names", () => { const groups = analyzeRepositoryGroups( Array.from({ length: 35 }, (_, index) => `packages/area-${index}/src/index.ts`), From 2ad0f90f2e4f9bdb13e2454e57cbac71d4c6c3c8 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 4/8] docs(tui): document group map checks and usage --- UserGuide.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/UserGuide.md b/UserGuide.md index 35da4767..1b15eb8e 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -4,7 +4,16 @@ ## Jay - X -## Cathy - X +## Cathy - Repository Learning Map (`/group`) + +Open an OpenCode session in a repository, select **Learn** mode, and enter `/group`. The map lists functionality groups. Select one to browse subgroups and files; use **Next page** or **Previous page** on long file lists. Choose **EXPLAIN THIS FUNCTIONALITY** or **EXPLAIN THIS SUBGROUP** to ask for a focused, read-only guide. The answer should cite files, explain relationships to other areas, suggest a reading order, and ask a check-your-understanding question. + +To see session-aware ordering, ask Learn to read a file, then run `/group` in the same session. Groups containing files read in this session are prioritized. If model-generated names are unavailable, directory-based names remain. The file inventory is capped at 10,000 paths, so very large repositories can show a partial map. Naming and explanations use model tokens. + +To user-test the feature, run `/group` in a repository containing `src/`, `test/`, or `packages/` files. Open a group and a subgroup, page through files, and request both explanations. Read a file and reopen the map to inspect its new ordering. In an empty repository, the command should show an informational message without crashing. The current mode should remain Learn throughout; use Build if you want OpenCode to modify files. + +Automated tests: [`packages/tui/test/util/repository-functionality.test.ts`](packages/tui/test/util/repository-functionality.test.ts) covers repository layouts, empty and generated-only inventories, 30-group and 20-subgroup limits, reference prioritization, prompt bounds, naming fallback, and path normalization. [`packages/tui/test/util/referenced-file.test.ts`](packages/tui/test/util/referenced-file.test.ts) covers completed reads, excluded results, de-duplication, and `/group` registration. These tests cover the issue's grouping, limit, relevance, and fallback criteria. The dialog and generated answer also need the manual checks above, because these unit tests do not exercise the complete TUI or model. From `packages/tui`, run `bun test test/util/repository-functionality.test.ts test/util/referenced-file.test.ts` and `bun typecheck`. + ## Janna - X From 06823ea7bb8efda38499fecb84773fa05fef2616 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 5/8] fix(tui): bound overflow subgroups without losing files --- packages/tui/src/util/repository-functionality.ts | 8 +++++++- .../tui/test/util/repository-functionality.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/util/repository-functionality.ts b/packages/tui/src/util/repository-functionality.ts index 94cdf22c..bbb13ef5 100644 --- a/packages/tui/src/util/repository-functionality.ts +++ b/packages/tui/src/util/repository-functionality.ts @@ -107,7 +107,13 @@ export function analyzeRepositoryGroups(input: readonly string[], referencedFile root: ".", files: rest.flatMap((item) => item.files), referencedFiles: rest.flatMap((item) => item.referencedFiles), - subgroups: rest.flatMap((item) => item.subgroups), + subgroups: limit(rest.flatMap((item) => item.subgroups), 20, (subgroups) => ({ + id: "other-repository-groups/other", + title: "Other areas", + root: ".", + files: subgroups.flatMap((item) => item.files), + referencedFiles: subgroups.flatMap((item) => item.referencedFiles), + })), })) } diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 802772d2..7e8d8740 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -72,6 +72,20 @@ describe("repository areas", () => { expect(buildGroupNamingPrompt(groups)).toContain("Group: packages/area-34") }) + test("caps subgroups in the overflow group without losing files", () => { + const files = Array.from({ length: 55 }, (_, index) => `packages/area-${index}/src/index.ts`) + const groups = analyzeRepositoryGroups(files, files) + const overflow = groups.at(-1)! + + expect(groups).toHaveLength(30) + expect(overflow.title).toBe("Other repository groups") + expect(overflow.subgroups).toHaveLength(20) + expect(overflow.subgroups.flatMap((subgroup) => subgroup.files).sort()).toEqual([...overflow.files].sort()) + expect(overflow.subgroups.flatMap((subgroup) => subgroup.referencedFiles).sort()).toEqual( + [...overflow.referencedFiles].sort(), + ) + }) + test("handles empty repositories and ignores generated-only inventories", () => { expect(analyzeRepositoryGroups([])).toEqual([]) expect(analyzeRepositoryGroups(["node_modules/pkg/index.js", "dist/app.js", ".git/config"])).toEqual([]) From 22db44b95e5cbe8cb696fc7b10a3e05848cc4170 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:21 -0400 Subject: [PATCH 6/8] fix(tui): match absolute read paths to repository files --- packages/tui/src/routes/session/index.tsx | 1 + packages/tui/src/util/referenced-file.ts | 6 ++++-- packages/tui/test/util/referenced-file.test.ts | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 284395a1..ef046410 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -478,6 +478,7 @@ export function Session() { run: async () => { const referencedFiles = collectReferencedFiles( messages().flatMap((message) => sync.data.part[message.id] ?? []), + project.instance.directory(), ) dialog.replace(() => ) const result = await sdk.client.find.files({ diff --git a/packages/tui/src/util/referenced-file.ts b/packages/tui/src/util/referenced-file.ts index d97c65fc..b0e40bd4 100644 --- a/packages/tui/src/util/referenced-file.ts +++ b/packages/tui/src/util/referenced-file.ts @@ -18,14 +18,16 @@ export const referencedFileCommand = { }, } as const -export function collectReferencedFiles(parts: readonly SessionPart[]) { +export function collectReferencedFiles(parts: readonly SessionPart[], directory = "") { const seen = new Set() + const root = directory.replaceAll("\\", "/").replace(/\/$/, "") return parts.flatMap((part) => { const filePath = referencedFilePath(part) if (!filePath) return [] - const normalized = filePath.replaceAll("\\", "/") + const path = filePath.replaceAll("\\", "/") + const normalized = root && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path if (seen.has(normalized)) return [] seen.add(normalized) return [normalized] diff --git a/packages/tui/test/util/referenced-file.test.ts b/packages/tui/test/util/referenced-file.test.ts index 5a0de1e2..f9ef5c21 100644 --- a/packages/tui/test/util/referenced-file.test.ts +++ b/packages/tui/test/util/referenced-file.test.ts @@ -52,6 +52,23 @@ describe("referenced files", () => { ).toEqual([]) }) + test("matches absolute read paths to relative repository files", () => { + expect( + collectReferencedFiles( + [ + completedRead("/home/student/project/src/index.ts"), + completedRead("src/index.ts"), + completedRead("/home/student/project-copy/src/other.ts"), + ], + "/home/student/project", + ), + ).toEqual(["src/index.ts", "/home/student/project-copy/src/other.ts"]) + + expect( + collectReferencedFiles([completedRead("C:\\repo\\src\\index.ts")], "C:\\repo"), + ).toEqual(["src/index.ts"]) + }) + test("registers /group", () => { expect(referencedFileCommand.slash.name).toBe("group") }) From 47084c8d57edd08108fd7a72cc26a42571c852d2 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 17:47:22 -0400 Subject: [PATCH 7/8] test(tui): trace completed reads into map prompts --- UserGuide.md | 6 +++-- .../test/integration/repository-map.test.ts | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 packages/tui/test/integration/repository-map.test.ts diff --git a/UserGuide.md b/UserGuide.md index 1b15eb8e..df8ee801 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -10,9 +10,11 @@ Open an OpenCode session in a repository, select **Learn** mode, and enter `/gro To see session-aware ordering, ask Learn to read a file, then run `/group` in the same session. Groups containing files read in this session are prioritized. If model-generated names are unavailable, directory-based names remain. The file inventory is capped at 10,000 paths, so very large repositories can show a partial map. Naming and explanations use model tokens. -To user-test the feature, run `/group` in a repository containing `src/`, `test/`, or `packages/` files. Open a group and a subgroup, page through files, and request both explanations. Read a file and reopen the map to inspect its new ordering. In an empty repository, the command should show an informational message without crashing. The current mode should remain Learn throughout; use Build if you want OpenCode to modify files. +**End-to-end test (manual):** In Learn mode, run `/group` in a repository containing `src/`, `test/`, or `packages/` files. Open a group and subgroup, page through files, and request both explanations. The answers should name real files, describe evidence-backed relationships, give a reading order, and ask a learning question. Ask Learn to read a file, reopen `/group`, and confirm its group moves toward the top. In an empty repository, `/group` should show an informational message without crashing. Learn should remain selected throughout. This manual test covers the complete TUI and model workflow; the automated tests below do not launch the full application. -Automated tests: [`packages/tui/test/util/repository-functionality.test.ts`](packages/tui/test/util/repository-functionality.test.ts) covers repository layouts, empty and generated-only inventories, 30-group and 20-subgroup limits, reference prioritization, prompt bounds, naming fallback, and path normalization. [`packages/tui/test/util/referenced-file.test.ts`](packages/tui/test/util/referenced-file.test.ts) covers completed reads, excluded results, de-duplication, and `/group` registration. These tests cover the issue's grouping, limit, relevance, and fallback criteria. The dialog and generated answer also need the manual checks above, because these unit tests do not exercise the complete TUI or model. From `packages/tui`, run `bun test test/util/repository-functionality.test.ts test/util/referenced-file.test.ts` and `bun typecheck`. +**Unit tests (automated):** [`packages/tui/test/util/repository-functionality.test.ts`](packages/tui/test/util/repository-functionality.test.ts) covers repository layouts, empty inventories, 30-group and 20-subgroup limits, overflow preservation, priority, prompt bounds, and naming fallback. [`packages/tui/test/util/referenced-file.test.ts`](packages/tui/test/util/referenced-file.test.ts) covers completed reads, excluded results, absolute and relative paths, de-duplication, and `/group` registration. + +**Integration test (automated):** [`packages/tui/test/integration/repository-map.test.ts`](packages/tui/test/integration/repository-map.test.ts) passes completed read records through path collection, repository grouping, and both naming and explanation prompts. It verifies that a real session path remains prioritized and appears as evidence in the resulting guides. From `packages/tui`, run `bun test test/util/repository-functionality.test.ts test/util/referenced-file.test.ts test/integration/repository-map.test.ts` and `bun typecheck`. ## Janna - X diff --git a/packages/tui/test/integration/repository-map.test.ts b/packages/tui/test/integration/repository-map.test.ts new file mode 100644 index 00000000..172cd813 --- /dev/null +++ b/packages/tui/test/integration/repository-map.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { collectReferencedFiles } from "../../src/util/referenced-file" +import { analyzeRepositoryGroups, buildFunctionalGroupPrompt, buildGroupNamingPrompt } from "../../src/util/repository-functionality" + +test("integration: completed session reads prioritize map groups and ground their guides", () => { + const referenced = collectReferencedFiles( + [ + { type: "tool", tool: "read", state: { status: "completed", input: { filePath: "/repo/apps/web/src/home.tsx" } } }, + { type: "tool", tool: "read", state: { status: "error", input: { filePath: "/repo/packages/api/src/user.ts" } } }, + ], + "/repo", + ) + const groups = analyzeRepositoryGroups( + ["packages/api/src/user.ts", "apps/web/src/home.tsx", "apps/web/test/home.test.ts"], + referenced, + ) + const web = groups[0] + + expect(referenced).toEqual(["apps/web/src/home.tsx"]) + expect(web.id).toBe("apps/web") + expect(web.referencedFiles).toEqual(referenced) + expect(buildGroupNamingPrompt(groups)).toContain("- apps/web/src/home.tsx") + expect(buildFunctionalGroupPrompt(web, groups)).toContain("- apps/web/src/home.tsx") + expect(buildFunctionalGroupPrompt(web.subgroups[0], groups)).toContain("- apps/web/src/home.tsx") +}) From 3c053285d97c21778f29ad2ebbd3c38a599871e9 Mon Sep 17 00:00:00 2001 From: yswcyswc Date: Thu, 8 Oct 2026 20:14:44 -0400 Subject: [PATCH 8/8] test(tui): deduplicate repeated reads in group guides --- .../tui/test/integration/repository-map.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/tui/test/integration/repository-map.test.ts b/packages/tui/test/integration/repository-map.test.ts index 172cd813..4ae6db34 100644 --- a/packages/tui/test/integration/repository-map.test.ts +++ b/packages/tui/test/integration/repository-map.test.ts @@ -23,3 +23,17 @@ test("integration: completed session reads prioritize map groups and ground thei expect(buildFunctionalGroupPrompt(web, groups)).toContain("- apps/web/src/home.tsx") expect(buildFunctionalGroupPrompt(web.subgroups[0], groups)).toContain("- apps/web/src/home.tsx") }) + +test("integration: repeated session reads count once in the map and guide", () => { + const reads = [ + { type: "tool", tool: "read", state: { status: "completed", input: { filePath: "/repo/apps/web/src/home.tsx" } } }, + { type: "tool", tool: "read", state: { status: "completed", input: { filePath: "apps/web/src/home.tsx" } } }, + ] + const referenced = collectReferencedFiles(reads, "/repo") + const [web] = analyzeRepositoryGroups(["apps/web/src/home.tsx", "apps/web/src/about.tsx"], referenced) + const prompt = buildFunctionalGroupPrompt(web, [web]) + + expect(referenced).toEqual(["apps/web/src/home.tsx"]) + expect(web.referencedFiles).toEqual(["apps/web/src/home.tsx"]) + expect(prompt.match(/- apps\/web\/src\/home\.tsx/g)).toHaveLength(1) +})