import { describe, it, expect } from 'vitest' import { buildWorkspaceTree, legacyScopeToPath, relativizeWorkspacePath } from './workspaceTree' import { dirKey, kindKey, leafKeyFor, type WorkspaceItem, type WorkspaceItemKind } from './workspacePicker' import type { DrillBranch, DrillLeaf, DrillNode } from './drillPicker' const item = ( kind: WorkspaceItemKind, path: string, summary?: string, raw_app?: boolean ): WorkspaceItem => ({ kind, path, summary: summary ?? '', raw_app }) const isBranch = (n: DrillNode | undefined): n is DrillBranch => !!n && n.type === 'branch' const isLeaf = (n: DrillNode | undefined): n is DrillLeaf => !!n && n.type === 'leaf' const childKeys = (b: DrillBranch) => b.children.map((c) => c.key) const findBranch = (nodes: DrillNode[], key: string): DrillBranch => { const n = nodes.find((x) => x.key === key) if (!isBranch(n)) throw new Error(`expected branch ${key} in [${nodes.map((x) => x.key)}]`) return n } describe('buildWorkspaceTree', () => { describe('shape', () => { it('returns an empty tree when kinds is empty', () => { expect(buildWorkspaceTree({ loaded: {}, kinds: [], loadingKind: {} })).toEqual([]) }) it('multi-kind: prepends an All branch then per-kind branches', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [item('script', 'f/demo/b')] }, kinds: ['flow', 'script'], loadingKind: {} }) expect(tree.map((n) => n.key)).toEqual([kindKey('all'), kindKey('flow'), kindKey('script')]) }) it('All branch is omitFromSearch and labeled "All"', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [] }, kinds: ['flow', 'script'], loadingKind: {} }) const all = findBranch(tree, kindKey('all')) expect(all.omitFromSearch).toBe(true) expect(all.label).toBe('All') }) it('per-kind branches are searchGroup anchors', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [] }, kinds: ['flow', 'script'], loadingKind: {} }) const flow = findBranch(tree, kindKey('flow')) expect(flow.searchGroup).toBe(true) }) it("single-kind: returns that kind branch's children directly (no kind-level)", () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a'), item('flow', 'u/alice/b')] }, kinds: ['flow'], loadingKind: {} }) // At the top we should see the scope dirs (u/alice, f/demo) directly, // not a single 'kind:flow' branch wrapping them. expect(tree.every((n) => isBranch(n) && n.key.startsWith('dir:flow:'))).toBe(true) // u-scopes come before f-scopes expect(tree.map((n) => n.key)).toEqual([dirKey('flow', 'u/alice'), dirKey('flow', 'f/demo')]) }) }) describe('loading state', () => { it('per-kind branch is loading=true when loaded[k] is undefined and loadingKind[k] is true', () => { const tree = buildWorkspaceTree({ loaded: {}, kinds: ['flow', 'script'], loadingKind: { flow: true } }) const flow = findBranch(tree, kindKey('flow')) expect(flow.loading).toBe(true) }) it('per-kind branch is not loading once loaded[k] is set, even mid-refetch', () => { const tree = buildWorkspaceTree({ loaded: { flow: [] }, kinds: ['flow', 'script'], loadingKind: { flow: true } }) const flow = findBranch(tree, kindKey('flow')) expect(flow.loading).toBeFalsy() }) it('All branch is loading when any kind is loading', () => { const tree = buildWorkspaceTree({ loaded: { flow: [] }, kinds: ['flow', 'script'], loadingKind: { script: true } }) const all = findBranch(tree, kindKey('all')) expect(all.loading).toBe(true) }) }) describe('dir forest', () => { it('groups leaves under their scope, then nested folders', () => { const tree = buildWorkspaceTree({ loaded: { flow: [ item('flow', 'f/demo/a'), item('flow', 'f/demo/sub/b'), item('flow', 'f/demo/sub/c'), item('flow', 'u/alice/d') ] }, kinds: ['flow'], loadingKind: {} }) // Top-level: u/alice (user scope), f/demo (folder scope) expect(tree.map((n) => n.key)).toEqual([dirKey('flow', 'u/alice'), dirKey('flow', 'f/demo')]) const demo = findBranch(tree, dirKey('flow', 'f/demo')) // Children: nested folder `sub` first, then leaf `a` expect(childKeys(demo)).toEqual([ dirKey('flow', 'f/demo/sub'), leafKeyFor('flow', 'f/demo/a') ]) const sub = findBranch(demo.children, dirKey('flow', 'f/demo/sub')) expect(childKeys(sub)).toEqual([ leafKeyFor('flow', 'f/demo/sub/b'), leafKeyFor('flow', 'f/demo/sub/c') ]) }) it('skips items with paths shorter than 3 segments', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo'), item('flow', 'f/demo/a')] }, kinds: ['flow'], loadingKind: {} }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) expect(childKeys(demo)).toEqual([leafKeyFor('flow', 'f/demo/a')]) }) }) describe('leaf shape', () => { it('uses summary as label and path as secondary when summary is present', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a', 'Hello')] }, kinds: ['flow'], loadingKind: {} }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) const leaf = demo.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.label).toBe('Hello') expect(leaf.secondary).toBe('f/demo/a') }) it('falls back to path as label when summary is empty', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')] }, kinds: ['flow'], loadingKind: {} }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) const leaf = demo.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.label).toBe('f/demo/a') expect(leaf.secondary).toBeUndefined() }) it('labels and groups a draft-only item by its friendly draftPath, keyed by storage path', () => { const draft = { ...item('app', 'u/admin/draft_abc123'), draftPath: 'f/marketing/dashboard' } const tree = buildWorkspaceTree({ loaded: { app: [draft] }, kinds: ['app'], loadingKind: {} }) // Grouped under the friendly folder, not u/admin. expect(tree.map((n) => n.key)).toEqual([dirKey('app', 'f/marketing')]) const marketing = findBranch(tree, dirKey('app', 'f/marketing')) const leaf = marketing.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') // Displayed by the friendly path; keyed (and navigated) by storage path. expect(leaf.label).toBe('f/marketing/dashboard') expect(leaf.key).toBe(leafKeyFor('app', 'u/admin/draft_abc123')) expect(leaf.data.path).toBe('u/admin/draft_abc123') }) it('uses the friendly draftPath as secondary when a summary is present', () => { const draft = { ...item('script', 'u/admin/draft_xyz', 'My Script'), draftPath: 'u/admin/my_script' } const tree = buildWorkspaceTree({ loaded: { script: [draft] }, kinds: ['script'], loadingKind: {} }) const admin = findBranch(tree, dirKey('script', 'u/admin')) const leaf = admin.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.label).toBe('My Script') expect(leaf.secondary).toBe('u/admin/my_script') }) it('marks the currentItem leaf with current=true', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a'), item('flow', 'f/demo/b')] }, kinds: ['flow'], loadingKind: {}, currentItem: item('flow', 'f/demo/a') }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) const [a, b] = demo.children if (!isLeaf(a) || !isLeaf(b)) throw new Error('expected leaves') expect(a.current).toBe(true) expect(b.current).toBeFalsy() }) }) describe('withCurrent: rename suppression', () => { it('injects currentItem at its live path when not already in the list', () => { const tree = buildWorkspaceTree({ loaded: { flow: [] }, kinds: ['flow'], loadingKind: {}, currentItem: { ...item('flow', 'f/demo/new'), summary: 'My Flow' } }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) expect(demo.children.map((c) => c.key)).toEqual([leafKeyFor('flow', 'f/demo/new')]) }) it('drops the savedPath entry during a mid-rename so only the live one shows', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/old', 'My Flow')] }, kinds: ['flow'], loadingKind: {}, currentItem: { ...item('flow', 'f/demo/new', 'My Flow'), savedPath: 'f/demo/old' } }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) const paths = demo.children.map((c) => c.key) expect(paths).toContain(leafKeyFor('flow', 'f/demo/new')) expect(paths).not.toContain(leafKeyFor('flow', 'f/demo/old')) }) it('does not duplicate a draft-only row whose friendly draftPath is the current live path', () => { // Editor open on a renamed draft-only script: currentItem.path is the // friendly path while listScripts returns the storage-path row carrying // the same friendly path as draftPath. One leaf, marked current. const loadedDraft = { ...item('script', 'u/admin/draft_abc'), draftPath: 'u/admin/my_script' } const tree = buildWorkspaceTree({ loaded: { script: [loadedDraft] }, kinds: ['script'], loadingKind: {}, currentItem: item('script', 'u/admin/my_script') }) const admin = findBranch(tree, dirKey('script', 'u/admin')) expect(admin.children.map((c) => c.key)).toEqual([leafKeyFor('script', 'u/admin/draft_abc')]) const leaf = admin.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.current).toBe(true) }) it('drops the storage-path row via draftPath during a mid-rename', () => { // Renaming a draft-only item: savedPath is the old friendly path, which // the loaded row only knows as its draftPath. The stale row must go so // only the live (typed) entry shows. const loadedDraft = { ...item('script', 'u/admin/draft_abc'), draftPath: 'u/admin/old_name' } const tree = buildWorkspaceTree({ loaded: { script: [loadedDraft] }, kinds: ['script'], loadingKind: {}, currentItem: { ...item('script', 'u/admin/new_name'), savedPath: 'u/admin/old_name' } }) const admin = findBranch(tree, dirKey('script', 'u/admin')) expect(admin.children.map((c) => c.key)).toEqual([leafKeyFor('script', 'u/admin/new_name')]) }) it('does not re-inject when the live entry already exists in loaded', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a', 'Original')] }, kinds: ['flow'], loadingKind: {}, currentItem: item('flow', 'f/demo/a', 'Original') }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) expect(demo.children.length).toBe(1) }) it('passes other-kind items through untouched', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [item('script', 'f/demo/b')] }, kinds: ['flow', 'script'], loadingKind: {}, currentItem: { ...item('flow', 'f/demo/new'), savedPath: 'f/demo/old' } }) const script = findBranch(tree, kindKey('script')) const demo = findBranch(script.children, dirKey('script', 'f/demo')) expect(demo.children.map((c) => c.key)).toEqual([leafKeyFor('script', 'f/demo/b')]) }) }) describe('extraItemsByKind (drafts)', () => { it('merges extras alongside loaded items', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')] }, kinds: ['flow'], loadingKind: {}, extraItemsByKind: { flow: [item('flow', 'f/demo/draft')] } }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) expect(demo.children.map((c) => c.key).sort()).toEqual( [leafKeyFor('flow', 'f/demo/a'), leafKeyFor('flow', 'f/demo/draft')].sort() ) }) it('drops extras whose path collides with a loaded item (loaded wins)', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a', 'Backend summary')] }, kinds: ['flow'], loadingKind: {}, extraItemsByKind: { flow: [item('flow', 'f/demo/a', 'Draft summary')] } }) const demo = findBranch(tree, dirKey('flow', 'f/demo')) expect(demo.children.length).toBe(1) const leaf = demo.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.label).toBe('Backend summary') }) it('extras flow into the cross-kind All branch too', () => { const tree = buildWorkspaceTree({ loaded: { flow: [], script: [item('script', 'f/demo/b')] }, kinds: ['flow', 'script'], loadingKind: {}, extraItemsByKind: { flow: [item('flow', 'f/demo/draft')] } }) const all = findBranch(tree, kindKey('all')) const demo = findBranch(all.children, dirKey('all', 'f/demo')) const keys = demo.children.map((c) => c.key) expect(keys).toContain(leafKeyFor('flow', 'f/demo/draft')) expect(keys).toContain(leafKeyFor('script', 'f/demo/b')) }) it('drops a live-cell extra at the friendly path when a loaded row carries it as draftPath', () => { // listApps returns the draft-only row at its storage path with the // friendly path in draftPath; the live editor cell surfaces the same // draft as an extra keyed by the friendly path. One leaf, not two. const loadedDraft = { ...item('app', 'u/admin/draft_abc'), draftPath: 'u/admin/dashboard' } const tree = buildWorkspaceTree({ loaded: { app: [loadedDraft] }, kinds: ['app'], loadingKind: {}, extraItemsByKind: { app: [item('app', 'u/admin/dashboard')] } }) const admin = findBranch(tree, dirKey('app', 'u/admin')) expect(admin.children.map((c) => c.key)).toEqual([leafKeyFor('app', 'u/admin/draft_abc')]) }) it('keeps one leaf per draft when two drafts share a friendly path', () => { const a = { ...item('flow', 'u/admin/draft_a'), draftPath: 'u/admin/fond_flow' } const b = { ...item('flow', 'u/admin/draft_b'), draftPath: 'u/admin/fond_flow' } const tree = buildWorkspaceTree({ loaded: { flow: [a, b] }, kinds: ['flow'], loadingKind: {}, // b's extra first, so a friendly-path match would fold it into a; // the repeat of a stands for a stale snapshot re-keyed twice. extraItemsByKind: { flow: [{ ...b }, { ...a }, { ...a }] } }) const admin = findBranch(tree, dirKey('flow', 'u/admin')) expect(admin.children.map((c) => c.key).sort()).toEqual( [leafKeyFor('flow', 'u/admin/draft_a'), leafKeyFor('flow', 'u/admin/draft_b')].sort() ) }) it('folds a mid-rename live extra into the stale loaded row: one storage-keyed leaf under the typed folder', () => { // Session picker while a rename's autosave is pending: listApps still // carries the pre-rename friendly path, the live cell extra (re-keyed to // the storage path by the picker) carries the typed one, and the tab's // currentItem is the storage path. The typed name must win, on a single // leaf that navigates via the storage path — never the display path. const staleLoaded = { ...item('app', 'u/admin/draft_abc'), draftPath: 'u/admin/old_name' } const liveExtra = { ...item('app', 'u/admin/draft_abc'), draftPath: 'f/marketing/new_name' } const tree = buildWorkspaceTree({ loaded: { app: [staleLoaded] }, kinds: ['app'], loadingKind: {}, extraItemsByKind: { app: [liveExtra] }, currentItem: item('app', 'u/admin/draft_abc') }) expect(tree.map((n) => n.key)).toEqual([dirKey('app', 'f/marketing')]) const marketing = findBranch(tree, dirKey('app', 'f/marketing')) expect(marketing.children.map((c) => c.key)).toEqual([leafKeyFor('app', 'u/admin/draft_abc')]) const leaf = marketing.children[0] if (!isLeaf(leaf)) throw new Error('expected leaf') expect(leaf.current).toBe(true) expect(leaf.data.path).toBe('u/admin/draft_abc') expect(leaf.label).toBe('f/marketing/new_name') }) it('is a no-op when extras are absent or empty', () => { const noOpts = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')] }, kinds: ['flow'], loadingKind: {} }) const emptyExtras = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')] }, kinds: ['flow'], loadingKind: {}, extraItemsByKind: { flow: [] } }) expect(JSON.stringify(noOpts)).toEqual(JSON.stringify(emptyExtras)) }) }) describe('flat layout', () => { it('roots the cross-kind scope dirs directly (no All / kind branches)', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a'), item('flow', 'u/alice/b')], script: [item('script', 'f/demo/c')] }, kinds: ['flow', 'script'], loadingKind: {}, layout: 'flat' }) // u-scopes before f-scopes, keyed under the 'all' namespace. expect(tree.map((n) => n.key)).toEqual([dirKey('all', 'u/alice'), dirKey('all', 'f/demo')]) }) it('mixes every kind inside the same scope dir', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [item('script', 'f/demo/b')] }, kinds: ['flow', 'script'], loadingKind: {}, layout: 'flat' }) const demo = findBranch(tree, dirKey('all', 'f/demo')) expect(childKeys(demo)).toEqual([ leafKeyFor('flow', 'f/demo/a'), leafKeyFor('script', 'f/demo/b') ]) }) it('applies extras and currentItem like the by-kind layout', () => { const tree = buildWorkspaceTree({ loaded: { flow: [item('flow', 'f/demo/a')], script: [] }, kinds: ['flow', 'script'], loadingKind: {}, extraItemsByKind: { script: [item('script', 'f/demo/draft')] }, currentItem: item('flow', 'f/demo/a'), layout: 'flat' }) const demo = findBranch(tree, dirKey('all', 'f/demo')) const leaves = demo.children.filter(isLeaf) expect(leaves.map((l) => l.key)).toEqual([ leafKeyFor('flow', 'f/demo/a'), leafKeyFor('script', 'f/demo/draft') ]) expect(leaves[0].current).toBe(true) }) }) }) describe('legacyScopeToPath', () => { it('returns [] for undefined scope', () => { expect(legacyScopeToPath(undefined, ['flow', 'script'])).toEqual([]) }) it('multi-kind: returns [kindKey] for a kind-only scope', () => { expect(legacyScopeToPath({ kind: 'flow' }, ['flow', 'script'])).toEqual([kindKey('flow')]) }) it('multi-kind: returns [kindKey, dirKey] for a kind+dir scope', () => { expect(legacyScopeToPath({ kind: 'flow', dir: 'f/demo' }, ['flow', 'script'])).toEqual([ kindKey('flow'), dirKey('flow', 'f/demo') ]) }) it('multi-kind: handles `all` as a kind', () => { expect(legacyScopeToPath({ kind: 'all', dir: 'f/demo' }, ['flow', 'script'])).toEqual([ kindKey('all'), dirKey('all', 'f/demo') ]) }) it('single-kind: returns [] for a kind-only scope (no kind level in tree)', () => { expect(legacyScopeToPath({ kind: 'flow' }, ['flow'])).toEqual([]) }) it('single-kind: returns [dirKey] for a kind+dir scope', () => { expect(legacyScopeToPath({ kind: 'flow', dir: 'f/demo' }, ['flow'])).toEqual([ dirKey('flow', 'f/demo') ]) }) it('flat: returns [dirKey under all] for a dir scope, ignoring the kind', () => { expect(legacyScopeToPath({ kind: 'flow', dir: 'f/demo' }, ['flow', 'script'], 'flat')).toEqual([ dirKey('all', 'f/demo') ]) }) it('flat: returns [] without a dir', () => { expect(legacyScopeToPath({ kind: 'all' }, ['flow', 'script'], 'flat')).toEqual([]) }) }) describe('relativizeWorkspacePath', () => { it('returns the absolute path when scope has no dir segment', () => { expect(relativizeWorkspacePath('f/demo/a', [])).toBe('f/demo/a') expect(relativizeWorkspacePath('f/demo/a', [kindKey('flow')])).toBe('f/demo/a') }) it('shortens to the path relative to the deepest dir scope', () => { const scope = [kindKey('flow'), dirKey('flow', 'f/demo')] expect(relativizeWorkspacePath('f/demo/a', scope)).toBe('a') }) it('uses the DEEPEST dir scope when there are nested ones', () => { const scope = [kindKey('flow'), dirKey('flow', 'f/demo'), dirKey('flow', 'f/demo/sub')] expect(relativizeWorkspacePath('f/demo/sub/b', scope)).toBe('b') }) it('falls back to absolute path when the leaf is not under the dir scope', () => { const scope = [kindKey('flow'), dirKey('flow', 'f/demo')] expect(relativizeWorkspacePath('f/other/a', scope)).toBe('f/other/a') }) })