Skip to content

Commit a4bc883

Browse files
authored
chore: refactoring and tests (anomalyco#12468)
1 parent c07077f commit a4bc883

39 files changed

+3798
-1488
lines changed

.github/workflows/test.yml

Lines changed: 45 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,45 @@ on:
77
pull_request:
88
workflow_dispatch:
99
jobs:
10-
test:
11-
name: test (${{ matrix.settings.name }})
10+
unit:
11+
name: unit (linux)
12+
runs-on: blacksmith-4vcpu-ubuntu-2404
13+
defaults:
14+
run:
15+
shell: bash
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
with:
20+
token: ${{ secrets.GITHUB_TOKEN }}
21+
22+
- name: Setup Bun
23+
uses: ./.github/actions/setup-bun
24+
25+
- name: Configure git identity
26+
run: |
27+
git config --global user.email "bot@opencode.ai"
28+
git config --global user.name "opencode"
29+
30+
- name: Run unit tests
31+
run: bun turbo test
32+
33+
e2e:
34+
name: e2e (${{ matrix.settings.name }})
35+
needs: unit
1236
strategy:
1337
fail-fast: false
1438
matrix:
1539
settings:
1640
- name: linux
1741
host: blacksmith-4vcpu-ubuntu-2404
1842
playwright: bunx playwright install --with-deps
19-
workdir: .
20-
command: |
21-
git config --global user.email "bot@opencode.ai"
22-
git config --global user.name "opencode"
23-
bun turbo test
2443
- name: windows
2544
host: blacksmith-4vcpu-windows-2025
2645
playwright: bunx playwright install
27-
workdir: packages/app
28-
command: bun test:e2e:local
2946
runs-on: ${{ matrix.settings.host }}
47+
env:
48+
PLAYWRIGHT_BROWSERS_PATH: 0
3049
defaults:
3150
run:
3251
shell: bash
@@ -43,87 +62,10 @@ jobs:
4362
working-directory: packages/app
4463
run: ${{ matrix.settings.playwright }}
4564

46-
- name: Set OS-specific paths
47-
run: |
48-
if [ "${{ runner.os }}" = "Windows" ]; then
49-
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}\\opencode-e2e" >> "$GITHUB_ENV"
50-
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}\\opencode-e2e\\home" >> "$GITHUB_ENV"
51-
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}\\opencode-e2e\\share" >> "$GITHUB_ENV"
52-
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV"
53-
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV"
54-
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV"
55-
else
56-
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV"
57-
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV"
58-
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}/opencode-e2e/share" >> "$GITHUB_ENV"
59-
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV"
60-
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV"
61-
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV"
62-
fi
63-
64-
- name: Seed opencode data
65-
if: matrix.settings.name != 'windows'
66-
working-directory: packages/opencode
67-
run: bun script/seed-e2e.ts
68-
env:
69-
OPENCODE_DISABLE_SHARE: "true"
70-
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
71-
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
72-
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
73-
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
74-
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
75-
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
76-
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
77-
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
78-
OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }}
79-
OPENCODE_E2E_SESSION_TITLE: "E2E Session"
80-
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e"
81-
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano"
82-
83-
- name: Run opencode server
84-
if: matrix.settings.name != 'windows'
85-
working-directory: packages/opencode
86-
run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 &
87-
env:
88-
OPENCODE_DISABLE_SHARE: "true"
89-
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
90-
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
91-
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
92-
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
93-
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
94-
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
95-
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
96-
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
97-
OPENCODE_CLIENT: "app"
98-
99-
- name: Wait for opencode server
100-
if: matrix.settings.name != 'windows'
101-
run: |
102-
for i in {1..120}; do
103-
curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0
104-
sleep 1
105-
done
106-
exit 1
107-
108-
- name: run
109-
working-directory: ${{ matrix.settings.workdir }}
110-
run: ${{ matrix.settings.command }}
65+
- name: Run app e2e tests
66+
run: bun --cwd packages/app test:e2e:local
11167
env:
11268
CI: true
113-
OPENCODE_DISABLE_SHARE: "true"
114-
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
115-
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
116-
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
117-
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
118-
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
119-
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
120-
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
121-
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
122-
PLAYWRIGHT_SERVER_HOST: "127.0.0.1"
123-
PLAYWRIGHT_SERVER_PORT: "4096"
124-
VITE_OPENCODE_SERVER_HOST: "127.0.0.1"
125-
VITE_OPENCODE_SERVER_PORT: "4096"
126-
OPENCODE_CLIENT: "app"
12769
timeout-minutes: 30
12870

12971
- name: Upload Playwright artifacts
@@ -136,3 +78,18 @@ jobs:
13678
path: |
13779
packages/app/e2e/test-results
13880
packages/app/e2e/playwright-report
81+
82+
required:
83+
name: test (linux)
84+
runs-on: blacksmith-4vcpu-ubuntu-2404
85+
needs:
86+
- unit
87+
- e2e
88+
if: always()
89+
steps:
90+
- name: Verify upstream test jobs passed
91+
run: |
92+
echo "unit=${{ needs.unit.result }}"
93+
echo "e2e=${{ needs.e2e.result }}"
94+
test "${{ needs.unit.result }}" = "success"
95+
test "${{ needs.e2e.result }}" = "success"

packages/app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"dev": "vite",
1515
"build": "vite build",
1616
"serve": "vite preview",
17-
"test": "playwright test",
17+
"test": "bun run test:unit",
18+
"test:unit": "bun test ./src",
1819
"test:e2e": "playwright test",
1920
"test:e2e:local": "bun script/e2e-local.ts",
2021
"test:e2e:ui": "playwright test --ui",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { beforeAll, describe, expect, mock, test } from "bun:test"
2+
3+
let shouldListRoot: typeof import("./file-tree").shouldListRoot
4+
let shouldListExpanded: typeof import("./file-tree").shouldListExpanded
5+
let dirsToExpand: typeof import("./file-tree").dirsToExpand
6+
7+
beforeAll(async () => {
8+
mock.module("@solidjs/router", () => ({
9+
useParams: () => ({}),
10+
}))
11+
mock.module("@/context/file", () => ({
12+
useFile: () => ({
13+
tree: {
14+
state: () => undefined,
15+
list: () => Promise.resolve(),
16+
children: () => [],
17+
expand: () => {},
18+
collapse: () => {},
19+
},
20+
}),
21+
}))
22+
mock.module("@opencode-ai/ui/collapsible", () => ({
23+
Collapsible: {
24+
Trigger: (props: { children?: unknown }) => props.children,
25+
Content: (props: { children?: unknown }) => props.children,
26+
},
27+
}))
28+
mock.module("@opencode-ai/ui/file-icon", () => ({ FileIcon: () => null }))
29+
mock.module("@opencode-ai/ui/icon", () => ({ Icon: () => null }))
30+
mock.module("@opencode-ai/ui/tooltip", () => ({ Tooltip: (props: { children?: unknown }) => props.children }))
31+
const mod = await import("./file-tree")
32+
shouldListRoot = mod.shouldListRoot
33+
shouldListExpanded = mod.shouldListExpanded
34+
dirsToExpand = mod.dirsToExpand
35+
})
36+
37+
describe("file tree fetch discipline", () => {
38+
test("root lists on mount unless already loaded or loading", () => {
39+
expect(shouldListRoot({ level: 0 })).toBe(true)
40+
expect(shouldListRoot({ level: 0, dir: { loaded: true } })).toBe(false)
41+
expect(shouldListRoot({ level: 0, dir: { loading: true } })).toBe(false)
42+
expect(shouldListRoot({ level: 1 })).toBe(false)
43+
})
44+
45+
test("nested dirs list only when expanded and stale", () => {
46+
expect(shouldListExpanded({ level: 1 })).toBe(false)
47+
expect(shouldListExpanded({ level: 1, dir: { expanded: false } })).toBe(false)
48+
expect(shouldListExpanded({ level: 1, dir: { expanded: true } })).toBe(true)
49+
expect(shouldListExpanded({ level: 1, dir: { expanded: true, loaded: true } })).toBe(false)
50+
expect(shouldListExpanded({ level: 1, dir: { expanded: true, loading: true } })).toBe(false)
51+
expect(shouldListExpanded({ level: 0, dir: { expanded: true } })).toBe(false)
52+
})
53+
54+
test("allowed auto-expand picks only collapsed dirs", () => {
55+
const expanded = new Set<string>()
56+
const filter = { dirs: new Set(["src", "src/components"]) }
57+
58+
const first = dirsToExpand({
59+
level: 0,
60+
filter,
61+
expanded: (dir) => expanded.has(dir),
62+
})
63+
64+
expect(first).toEqual(["src", "src/components"])
65+
66+
for (const dir of first) expanded.add(dir)
67+
68+
const second = dirsToExpand({
69+
level: 0,
70+
filter,
71+
expanded: (dir) => expanded.has(dir),
72+
})
73+
74+
expect(second).toEqual([])
75+
expect(dirsToExpand({ level: 1, filter, expanded: () => false })).toEqual([])
76+
})
77+
})

packages/app/src/components/file-tree.tsx

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createMemo,
99
For,
1010
Match,
11+
on,
1112
Show,
1213
splitProps,
1314
Switch,
@@ -25,6 +26,34 @@ type Filter = {
2526
dirs: Set<string>
2627
}
2728

29+
export function shouldListRoot(input: { level: number; dir?: { loaded?: boolean; loading?: boolean } }) {
30+
if (input.level !== 0) return false
31+
if (input.dir?.loaded) return false
32+
if (input.dir?.loading) return false
33+
return true
34+
}
35+
36+
export function shouldListExpanded(input: {
37+
level: number
38+
dir?: { expanded?: boolean; loaded?: boolean; loading?: boolean }
39+
}) {
40+
if (input.level === 0) return false
41+
if (!input.dir?.expanded) return false
42+
if (input.dir.loaded) return false
43+
if (input.dir.loading) return false
44+
return true
45+
}
46+
47+
export function dirsToExpand(input: {
48+
level: number
49+
filter?: { dirs: Set<string> }
50+
expanded: (dir: string) => boolean
51+
}) {
52+
if (input.level !== 0) return []
53+
if (!input.filter) return []
54+
return [...input.filter.dirs].filter((dir) => !input.expanded(dir))
55+
}
56+
2857
export default function FileTree(props: {
2958
path: string
3059
class?: string
@@ -111,19 +140,30 @@ export default function FileTree(props: {
111140

112141
createEffect(() => {
113142
const current = filter()
114-
if (!current) return
115-
if (level !== 0) return
116-
117-
for (const dir of current.dirs) {
118-
const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
119-
if (expanded) continue
120-
file.tree.expand(dir)
121-
}
143+
const dirs = dirsToExpand({
144+
level,
145+
filter: current,
146+
expanded: (dir) => untrack(() => file.tree.state(dir)?.expanded) ?? false,
147+
})
148+
for (const dir of dirs) file.tree.expand(dir)
122149
})
123150

151+
createEffect(
152+
on(
153+
() => props.path,
154+
(path) => {
155+
const dir = untrack(() => file.tree.state(path))
156+
if (!shouldListRoot({ level, dir })) return
157+
void file.tree.list(path)
158+
},
159+
{ defer: false },
160+
),
161+
)
162+
124163
createEffect(() => {
125-
const path = props.path
126-
untrack(() => void file.tree.list(path))
164+
const dir = file.tree.state(props.path)
165+
if (!shouldListExpanded({ level, dir })) return
166+
void file.tree.list(props.path)
127167
})
128168

129169
const nodes = createMemo(() => {

0 commit comments

Comments
 (0)