Skip to content

Commit 949c560

Browse files
committed
feat: installer mode selection
Signed-off-by: Sam Gammon <sam@elide.ventures>
1 parent 8b013f1 commit 949c560

21 files changed

Lines changed: 1193 additions & 231 deletions

__tests__/install-msi.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, it, expect, beforeEach, jest, mock } from 'bun:test'
2+
3+
// Create mock functions
4+
const execMock = jest.fn().mockResolvedValue(0)
5+
const whichMock = jest.fn().mockResolvedValue('C:\\Elide\\bin\\elide.exe')
6+
const downloadToolMock = jest.fn().mockResolvedValue('/tmp/elide.msi')
7+
const obtainVersionMock = jest.fn().mockResolvedValue('1.0.0')
8+
const buildCdnAssetUrlMock = jest
9+
.fn()
10+
.mockReturnValue(
11+
new URL(
12+
'https://elide.zip/artifacts/nightly/latest/elide.windows-amd64.msi?source=gha'
13+
)
14+
)
15+
const infoMock = jest.fn()
16+
const debugMock = jest.fn()
17+
18+
// Mock modules before import
19+
mock.module('@actions/exec', () => ({
20+
exec: execMock,
21+
getExecOutput: jest.fn()
22+
}))
23+
mock.module('@actions/io', () => ({
24+
which: whichMock,
25+
mv: jest.fn(),
26+
cp: jest.fn(),
27+
rmRF: jest.fn(),
28+
mkdirP: jest.fn()
29+
}))
30+
mock.module('@actions/core', () => ({
31+
info: infoMock,
32+
debug: debugMock,
33+
error: jest.fn(),
34+
warning: jest.fn(),
35+
getInput: jest.fn().mockReturnValue(''),
36+
getBooleanInput: jest.fn().mockReturnValue(true),
37+
setFailed: jest.fn(),
38+
setOutput: jest.fn(),
39+
addPath: jest.fn()
40+
}))
41+
mock.module('@actions/tool-cache', () => ({
42+
downloadTool: downloadToolMock,
43+
extractTar: jest.fn(),
44+
extractZip: jest.fn(),
45+
cacheDir: jest.fn(),
46+
find: jest.fn()
47+
}))
48+
mock.module('../src/command', () => ({
49+
obtainVersion: obtainVersionMock,
50+
elideInfo: jest.fn()
51+
}))
52+
mock.module('../src/releases', () => ({
53+
buildCdnAssetUrl: buildCdnAssetUrlMock
54+
}))
55+
56+
const { installViaMsi } = await import('../src/install-msi')
57+
const { default: buildOptions } = await import('../src/options')
58+
59+
describe('install-msi', () => {
60+
beforeEach(() => {
61+
execMock.mockClear()
62+
whichMock.mockClear()
63+
downloadToolMock.mockClear()
64+
obtainVersionMock.mockClear()
65+
buildCdnAssetUrlMock.mockClear()
66+
infoMock.mockClear()
67+
debugMock.mockClear()
68+
69+
downloadToolMock.mockResolvedValue('/tmp/elide.msi')
70+
execMock.mockResolvedValue(0)
71+
whichMock.mockResolvedValue('C:\\Elide\\bin\\elide.exe')
72+
obtainVersionMock.mockResolvedValue('1.0.0')
73+
buildCdnAssetUrlMock.mockReturnValue(
74+
new URL(
75+
'https://elide.zip/artifacts/nightly/latest/elide.windows-amd64.msi?source=gha'
76+
)
77+
)
78+
})
79+
80+
it('should construct the CDN URL with msi extension', async () => {
81+
const options = buildOptions({
82+
os: 'windows',
83+
arch: 'amd64',
84+
version: 'latest'
85+
})
86+
await installViaMsi(options)
87+
88+
expect(buildCdnAssetUrlMock).toHaveBeenCalledWith(options, 'msi')
89+
})
90+
91+
it('should call msiexec with correct arguments', async () => {
92+
const options = buildOptions({
93+
os: 'windows',
94+
arch: 'amd64',
95+
version: 'latest'
96+
})
97+
await installViaMsi(options)
98+
99+
expect(downloadToolMock).toHaveBeenCalledWith(
100+
'https://elide.zip/artifacts/nightly/latest/elide.windows-amd64.msi?source=gha'
101+
)
102+
expect(execMock).toHaveBeenCalledWith('msiexec', [
103+
'/i',
104+
'/tmp/elide.msi',
105+
'/quiet',
106+
'/norestart',
107+
`INSTALLDIR=${options.install_path}`
108+
])
109+
})
110+
111+
it('should return correct elidePath and version', async () => {
112+
const options = buildOptions({
113+
os: 'windows',
114+
arch: 'amd64',
115+
version: 'latest'
116+
})
117+
const result = await installViaMsi(options)
118+
119+
expect(result.elidePath).toBe('C:\\Elide\\bin\\elide.exe')
120+
expect(result.version.tag_name).toBe('1.0.0')
121+
expect(result.version.userProvided).toBe(false)
122+
})
123+
124+
it('should mark version as userProvided when not latest', async () => {
125+
const options = buildOptions({
126+
os: 'windows',
127+
arch: 'amd64',
128+
version: '1.2.3'
129+
})
130+
const result = await installViaMsi(options)
131+
132+
expect(result.version.userProvided).toBe(true)
133+
})
134+
})
Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ import { describe, it, expect, beforeEach, jest, mock } from 'bun:test'
33
// Create mock functions
44
const execMock = jest.fn().mockResolvedValue(0)
55
const whichMock = jest.fn().mockResolvedValue('/usr/local/bin/elide')
6-
const downloadToolMock = jest.fn().mockResolvedValue('/tmp/install.sh')
6+
const downloadToolMock = jest.fn().mockResolvedValue('/tmp/elide.pkg')
77
const obtainVersionMock = jest.fn().mockResolvedValue('1.0.0')
8-
const addPathMock = jest.fn()
8+
const buildCdnAssetUrlMock = jest
9+
.fn()
10+
.mockReturnValue(
11+
new URL(
12+
'https://elide.zip/artifacts/nightly/latest/elide.macos-arm64.pkg?source=gha'
13+
)
14+
)
915
const infoMock = jest.fn()
1016
const debugMock = jest.fn()
1117

@@ -27,9 +33,10 @@ mock.module('@actions/core', () => ({
2733
error: jest.fn(),
2834
warning: jest.fn(),
2935
getInput: jest.fn().mockReturnValue(''),
36+
getBooleanInput: jest.fn().mockReturnValue(true),
3037
setFailed: jest.fn(),
3138
setOutput: jest.fn(),
32-
addPath: addPathMock
39+
addPath: jest.fn()
3340
}))
3441
mock.module('@actions/tool-cache', () => ({
3542
downloadTool: downloadToolMock,
@@ -42,70 +49,76 @@ mock.module('../src/command', () => ({
4249
obtainVersion: obtainVersionMock,
4350
elideInfo: jest.fn()
4451
}))
52+
mock.module('../src/releases', () => ({
53+
buildCdnAssetUrl: buildCdnAssetUrlMock
54+
}))
4555

46-
const { installViaScript } = await import('../src/install-script')
56+
const { installViaPkg } = await import('../src/install-pkg')
4757
const { default: buildOptions } = await import('../src/options')
4858

49-
describe('install-script', () => {
59+
describe('install-pkg', () => {
5060
beforeEach(() => {
5161
execMock.mockClear()
5262
whichMock.mockClear()
5363
downloadToolMock.mockClear()
5464
obtainVersionMock.mockClear()
55-
addPathMock.mockClear()
65+
buildCdnAssetUrlMock.mockClear()
5666
infoMock.mockClear()
5767
debugMock.mockClear()
5868

59-
downloadToolMock.mockResolvedValue('/tmp/install.sh')
69+
downloadToolMock.mockResolvedValue('/tmp/elide.pkg')
6070
execMock.mockResolvedValue(0)
6171
whichMock.mockResolvedValue('/usr/local/bin/elide')
6272
obtainVersionMock.mockResolvedValue('1.0.0')
73+
buildCdnAssetUrlMock.mockReturnValue(
74+
new URL(
75+
'https://elide.zip/artifacts/nightly/latest/elide.macos-arm64.pkg?source=gha'
76+
)
77+
)
6378
})
6479

65-
it('should download and execute the install script', async () => {
80+
it('should construct the URL correctly via buildCdnAssetUrl', async () => {
6681
const options = buildOptions({
6782
os: 'darwin',
6883
arch: 'aarch64',
6984
version: 'latest'
7085
})
71-
const result = await installViaScript(options)
86+
await installViaPkg(options)
7287

73-
expect(downloadToolMock).toHaveBeenCalledWith(
74-
'https://dl.elide.dev/cli/install.sh'
75-
)
76-
expect(execMock).toHaveBeenCalledWith('bash', ['/tmp/install.sh'])
77-
expect(result.elidePath).toBe('/usr/local/bin/elide')
78-
expect(result.version.tag_name).toBe('1.0.0')
88+
expect(buildCdnAssetUrlMock).toHaveBeenCalledWith(options, 'pkg')
7989
})
8090

81-
it('should pass --version when a specific version is requested', async () => {
91+
it('should call sudo installer -pkg with the downloaded path', async () => {
8292
const options = buildOptions({
83-
os: 'linux',
84-
arch: 'amd64',
85-
version: '1.2.3'
93+
os: 'darwin',
94+
arch: 'aarch64',
95+
version: 'latest'
8696
})
87-
await installViaScript(options)
97+
await installViaPkg(options)
8898

89-
expect(execMock).toHaveBeenCalledWith('bash', [
90-
'/tmp/install.sh',
91-
'--version',
92-
'1.2.3'
99+
expect(downloadToolMock).toHaveBeenCalledWith(
100+
'https://elide.zip/artifacts/nightly/latest/elide.macos-arm64.pkg?source=gha'
101+
)
102+
expect(execMock).toHaveBeenCalledWith('sudo', [
103+
'installer',
104+
'-pkg',
105+
'/tmp/elide.pkg',
106+
'-target',
107+
'/'
93108
])
94109
})
95110

96-
it('should fall back to ~/.elide/bin if which fails initially', async () => {
97-
whichMock
98-
.mockRejectedValueOnce(new Error('not found'))
99-
.mockResolvedValueOnce('/home/runner/.elide/bin/elide')
100-
111+
it('should return the correct elidePath and version', async () => {
101112
const options = buildOptions({
102-
os: 'linux',
103-
arch: 'amd64',
113+
os: 'darwin',
114+
arch: 'aarch64',
104115
version: 'latest'
105116
})
106-
const result = await installViaScript(options)
117+
const result = await installViaPkg(options)
107118

108-
expect(addPathMock).toHaveBeenCalled()
109-
expect(result.elidePath).toBe('/home/runner/.elide/bin/elide')
119+
expect(result.elidePath).toBe('/usr/local/bin/elide')
120+
expect(result.version.tag_name).toBe('1.0.0')
121+
expect(result.elideBin).toBe('/usr/local/bin')
122+
expect(result.elideHome).toBe('/usr/local/bin')
110123
})
111124
})

0 commit comments

Comments
 (0)