diff --git a/UserGuide.md b/UserGuide.md index cd2aea47..20cc7cff 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -87,11 +87,37 @@ These tests cover the main input choices, the connection between the command, qu ## Cathy - Repository Learning Map (`/group`) -In an OpenCode repository session, select **Learn** and run `/group`. Open a functionality group to browse its subgroups and files (18 per page), or select **EXPLAIN THIS FUNCTIONALITY** / **EXPLAIN THIS SUBGROUP** for file-based guidance, relationships, a reading order, and a learning question. If Learn has read a file in this session, rerun `/group` to prioritize its group. Structural names remain if model naming fails; inventories over 10,000 files may be partial. +The `/group` command helps students explore an unfamiliar repository through functionality groups, subgroups, and file lists. It prioritizes groups containing files read in the current session and can explain their responsibilities and relationships. -**Manual end-to-end check:** Browse a group and subgroup, change pages, and request both explanations. Check that the answers cite real files and that a newly read file moves its group up. An empty repository should show a message without crashing. This checks the live model response. +### How to use it + +Open a repository session, select **Learn**, and run `/group`. Select a group and subgroup to browse files (18 per page), or choose **EXPLAIN THIS FUNCTIONALITY** / **EXPLAIN THIS SUBGROUP** for guidance, a reading order, and a learning question. + +Rerun `/group` after Learn reads files to refresh prioritization. Structural names remain if model naming fails; inventories over 10,000 files may be partial. + +### User testing + +1. In an active Learn session, run `/group` and confirm the Repository Learning Map appears. +2. Open a group and subgroup; check file paths, pagination, and back navigation. +3. Request both explanation types. Verify cited files and relationships, a reading order, and a learning question. +4. Ask Learn to read a file, then rerun `/group`. Groups with more distinct session-read files should rank higher. +5. Try an empty repository; confirm a message appears without crashing. + +### Automated tests + +- [Grouping tests](packages/tui/test/util/repository-functionality.test.ts) cover layouts, limits, file preservation, prioritization, prompts, and naming fallback. +- [Read-collection tests](packages/tui/test/util/referenced-file.test.ts) cover completed reads, path normalization, deduplication, and command registration. +- [Integration tests](packages/tui/test/integration/repository-map.test.ts) trace session reads through grouping and explanation prompts. +- [TUI end-to-end tests](packages/tui/test/group-map.e2e.test.ts) use a mock server to verify navigation, both explanation requests through Learn, and empty repositories. + +Run from `packages/tui`: + +```bash +bun test test/util/repository-functionality.test.ts test/util/referenced-file.test.ts test/integration/repository-map.test.ts test/group-map.e2e.test.ts +bun typecheck +``` -**Automated tests:** [Unit grouping](packages/tui/test/util/repository-functionality.test.ts) covers layouts, limits, prioritization, prompts, and fallback; [unit read collection](packages/tui/test/util/referenced-file.test.ts) covers completed reads, paths, and `/group` registration. The [integration test](packages/tui/test/integration/repository-map.test.ts) traces a read through grouping and explanation prompts. The [TUI E2E test](packages/tui/test/group-map.e2e.test.ts) uses a mock server to open `/group`, navigate files, and submit both explanation prompts in Learn. Together these cover #12's acceptance criteria; the manual check covers live model output. From `packages/tui`, run `bun test` and `bun typecheck`. +These tests cover the grouping and explanation workflow, including boundary and fallback cases. Manual checks verify live model answers, which the mock-server tests cannot assess. ## Janna - Code Explanations and Suggestions diff --git a/packages/tui/src/util/repository-functionality.ts b/packages/tui/src/util/repository-functionality.ts index bbb13ef5..ef372798 100644 --- a/packages/tui/src/util/repository-functionality.ts +++ b/packages/tui/src/util/repository-functionality.ts @@ -49,6 +49,13 @@ function rank(areas: T[]) { ) } +function commonDirectory(files: readonly string[]) { + const directories = files.map((file) => file.split("/").slice(0, -1)) + const first = directories[0] ?? [] + const difference = first.findIndex((part, index) => directories.some((directory) => directory[index] !== part)) + return first.slice(0, difference < 0 ? first.length : difference).join("/") || "." +} + function limit(areas: T[], maximum: number, overflow: (rest: T[]) => T) { const ordered = rank(areas) if (ordered.length <= maximum) return ordered @@ -92,6 +99,8 @@ export function analyzeRepositoryGroups(input: readonly string[], referencedFile if (referenced.has(file)) subgroup.referencedFiles.push(file) subgroups.set(root, subgroup) } + // Subgroup IDs combine related source and test areas; they are not filesystem paths. + for (const subgroup of subgroups.values()) subgroup.root = commonDirectory(subgroup.files) group.subgroups = limit([...subgroups.values()], 20, (rest) => ({ id: `${group.id}/other`, title: "Other areas", @@ -136,7 +145,7 @@ export function applyGroupNames(groups: readonly FunctionalGroup[], response: st > return groups.map((group) => { const title = names[group.id] - return typeof title === "string" ? { ...group, title: title.trim().slice(0, 50) } : group + return typeof title === "string" && title.trim() ? { ...group, title: title.trim().slice(0, 50) } : group }) } catch { return [...groups] diff --git a/packages/tui/test/util/repository-functionality.test.ts b/packages/tui/test/util/repository-functionality.test.ts index 7e8d8740..3b558ebf 100644 --- a/packages/tui/test/util/repository-functionality.test.ts +++ b/packages/tui/test/util/repository-functionality.test.ts @@ -7,6 +7,34 @@ import { } from "../../src/util/repository-functionality" describe("repository areas", () => { + test("subgroup prompts use a shared real directory instead of the logical group id", () => { + const [group] = analyzeRepositoryGroups([ + "packages/api/src/auth/token.ts", + "packages/api/test/auth/token.test.ts", + ]) + const subgroup = group.subgroups[0] + + expect(subgroup.id).toBe("packages/api/auth") + expect(subgroup.root).toBe("packages/api") + const prompt = buildFunctionalGroupPrompt(subgroup, [group]) + expect(prompt).toContain("Area root: packages/api\n") + expect(prompt).not.toContain("Area root: packages/api/auth") + expect(prompt).toContain("packages/api/src/auth/token.ts") + expect(prompt).toContain("packages/api/test/auth/token.test.ts") + }) + + test("subgroup roots preserve nested directories and handle project-root files", () => { + const [nested] = analyzeRepositoryGroups(["packages/api/src/auth/token.ts", "packages/api/src/auth/user.ts"]) + expect(nested.subgroups[0].root).toBe("packages/api/src/auth") + const [root] = analyzeRepositoryGroups(["README.md", "LICENSE"]) + expect(root.subgroups[0].root).toBe(".") + }) + + test.each(["", " ", "\t\n"])("retains the structural name for blank model title %j", (title) => { + const groups = analyzeRepositoryGroups(["packages/api/src/main.ts"]) + expect(applyGroupNames(groups, JSON.stringify({ "packages/api": title }))).toEqual(groups) + }) + test("groups common repository layouts without project-specific names", () => { const groups = analyzeRepositoryGroups([ "packages/api/src/auth/token.ts", diff --git a/turbo.json b/turbo.json index f59711fa..7544901b 100644 --- a/turbo.json +++ b/turbo.json @@ -22,6 +22,10 @@ // }, // Only add the package(s) your change actually touches — don't add all // of them, that defeats the point of keeping CI fast. + "@opencode-ai/tui#test": { + "dependsOn": ["^build"], + "outputs": [] + }, "opencode#test": { "dependsOn": ["^build"], "outputs": [],