mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: zhaohao1004 <zhaohao1004@gmail.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: FFXN <lizy@dify.ai>
140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
|
|
const SCRIPT = fileURLToPath(new URL('./install-r2.ps1', import.meta.url))
|
|
const hasPwsh = spawnSync('pwsh', ['-v'], { encoding: 'utf8' }).status === 0
|
|
// Each test starts pwsh; cold starts on a busy CI runner can exceed the default 5 seconds.
|
|
if (hasPwsh) vi.setConfig({ testTimeout: 15_000 })
|
|
const d = hasPwsh ? describe : describe.skip
|
|
|
|
const MANIFEST = JSON.stringify({
|
|
schema: 1,
|
|
name: 'difyctl',
|
|
channel: 'edge',
|
|
version: '0.1.0-edge.2fd7b82',
|
|
baseUrl: 'https://pub.example.r2.dev/difyctl/edge/0.1.0-edge.2fd7b82',
|
|
targets: {
|
|
'windows-x64': { asset: 'difyctl-v0.1.0-edge.2fd7b82-windows-x64.exe', sha256: 'deadbeef' },
|
|
},
|
|
})
|
|
|
|
function pwsh(
|
|
program: string,
|
|
env: Record<string, string> = {},
|
|
): { code: number; stdout: string; stderr: string } {
|
|
const full = `. '${SCRIPT}'\n${program}`
|
|
const r = spawnSync('pwsh', ['-NoProfile', '-Command', full], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, DIFYCTL_INSTALL_LIB: '1', ...env },
|
|
})
|
|
return {
|
|
code: r.status ?? 1,
|
|
stdout: (r.stdout ?? '').replace(/\r\n/g, '\n').trim(),
|
|
stderr: r.stderr ?? '',
|
|
}
|
|
}
|
|
|
|
d('install-r2.ps1', () => {
|
|
it('parses a target asset + sha from the manifest', () => {
|
|
const prog =
|
|
`$m = ConvertFrom-Json @'\n${MANIFEST}\n'@\n` +
|
|
`Write-Output (Get-TargetField $m 'windows-x64' 'asset')\n` +
|
|
`Write-Output (Get-TargetField $m 'windows-x64' 'sha256')`
|
|
const { stdout } = pwsh(prog)
|
|
expect(stdout).toBe('difyctl-v0.1.0-edge.2fd7b82-windows-x64.exe\ndeadbeef')
|
|
})
|
|
|
|
it('errors when DIFYCTL_R2_BASE is unset', () => {
|
|
const r = spawnSync('pwsh', ['-NoProfile', '-File', SCRIPT], {
|
|
encoding: 'utf8',
|
|
env: { ...process.env, DIFYCTL_R2_BASE: '' },
|
|
})
|
|
if (hasPwsh) {
|
|
expect(r.status).not.toBe(0)
|
|
expect(r.stderr + r.stdout).toMatch(/DIFYCTL_R2_BASE/)
|
|
}
|
|
})
|
|
})
|
|
|
|
d('install-r2.ps1 install location, PATH hint and download', () => {
|
|
const where = (env: Record<string, string>) =>
|
|
pwsh('Write-Output (Resolve-BinDir)', {
|
|
DIFYCTL_PREFIX: '',
|
|
DIFYCTL_INSTALL_DIR: '',
|
|
LOCALAPPDATA: '/lad',
|
|
...env,
|
|
}).stdout
|
|
|
|
it('defaults to %LOCALAPPDATA%\\difyctl\\bin', () => {
|
|
expect(where({})).toBe(join('/lad', 'difyctl', 'bin'))
|
|
})
|
|
|
|
it('accepts DIFYCTL_PREFIX like the GitHub installer', () => {
|
|
expect(where({ DIFYCTL_PREFIX: '/opt/dify' })).toBe(join('/opt/dify', 'bin'))
|
|
})
|
|
|
|
it('keeps DIFYCTL_INSTALL_DIR as the bin folder itself, over DIFYCTL_PREFIX', () => {
|
|
expect(where({ DIFYCTL_PREFIX: '/opt/dify', DIFYCTL_INSTALL_DIR: '/custom/bin' })).toBe(
|
|
'/custom/bin',
|
|
)
|
|
})
|
|
|
|
it('the PATH command adds to the user PATH, not the whole session PATH', () => {
|
|
const hint = pwsh(`Write-Output (Get-PathHint 'C:\\dify\\bin')`).stdout
|
|
expect(hint).toContain(
|
|
`"C:\\dify\\bin;" + [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
)
|
|
expect(hint).not.toContain('$env:PATH')
|
|
})
|
|
|
|
it('stops before downloading when the checksum is missing', () => {
|
|
const r = pwsh(
|
|
"function Invoke-WebRequest { throw 'must not download' }\n" +
|
|
"try { Install-DifyctlBinary 'https://x/difyctl.exe' '' '1.0.0' 'edge' '/tmp/none' } " +
|
|
'catch { Write-Output $_.Exception.Message }',
|
|
)
|
|
expect(r.stdout).toMatch(/^no checksum published for https:\/\/x\/difyctl\.exe/)
|
|
})
|
|
|
|
it('downloads to a unique temp file, not one named after the asset', () => {
|
|
const r = pwsh(
|
|
'function Invoke-WebRequest { param($Uri, $OutFile) Write-Output "OUT=$OutFile"; Set-Content -Path $OutFile -Value x }\n' +
|
|
"try { Install-DifyctlBinary 'https://x/difyctl.exe' 'deadbeef' '1.0.0' 'edge' '/tmp/none' } " +
|
|
'catch { Write-Output $_.Exception.Message }',
|
|
)
|
|
expect(r.stdout).toMatch(/OUT=.*difyctl-[0-9a-f]{32}\.exe\n/)
|
|
expect(r.stdout).toMatch(/checksum mismatch/)
|
|
})
|
|
})
|
|
|
|
// A v2 binary answers `install skills --help`; a v1 one exits 1 with "unknown command".
|
|
// The fakes are shell scripts, so this runs where pwsh runs outside Windows.
|
|
describe.skipIf(!hasPwsh || process.platform === 'win32')('install-r2.ps1 Write-SkillHint', () => {
|
|
const hint = (bin: string) => pwsh(`Write-SkillHint '${bin}'`)
|
|
|
|
it('prints the hint for a binary that has install skills, and stays quiet otherwise', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'difyctl-hint-'))
|
|
try {
|
|
const v2 = join(dir, 'v2')
|
|
const v1 = join(dir, 'v1')
|
|
writeFileSync(v2, '#!/bin/sh\n[ "$*" = "install skills --help" ]\n', { mode: 0o755 })
|
|
writeFileSync(v1, '#!/bin/sh\necho "unknown command: $*" >&2\nexit 1\n', { mode: 0o755 })
|
|
expect(hint(v2)).toMatchObject({
|
|
code: 0,
|
|
stdout: expect.stringContaining('difyctl install skills <skills root>'),
|
|
})
|
|
expect(hint(v1)).toMatchObject({ code: 0, stdout: '' })
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('prints nothing when the binary cannot run', () => {
|
|
expect(hint('/nonexistent/difyctl')).toMatchObject({ code: 0, stdout: '' })
|
|
})
|
|
})
|