Files
dify/cli/src/cache/app-info.test.ts
Yunlu Wen a728e0ac69 feat: adding dify cli (#36348)
Co-authored-by: GareArc <garethcxy@dify.ai>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: L1nSn0w <l1nsn0w@qq.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: gigglewang <gigglewang@dify.ai>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
2026-05-26 01:12:36 +00:00

112 lines
3.9 KiB
TypeScript

import type { AppMeta } from '../types/app-meta.js'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { FieldInfo, FieldParameters } from '../types/app-meta.js'
import { APP_INFO_TTL_MS, cachePath, loadAppInfoCache } from './app-info.js'
function metaInfoOnly(): AppMeta {
return {
info: {
id: 'app-1',
name: 'Greeter',
description: '',
mode: 'chat',
author: 'tester',
tags: [],
updated_at: undefined,
service_api_enabled: false,
is_agent: false,
},
parameters: null,
inputSchema: null,
coveredFields: new Set([FieldInfo]),
}
}
describe('app-info disk cache', () => {
let dir: string
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'difyctl-cache-'))
})
afterEach(async () => {
await rm(dir, { recursive: true, force: true })
})
it('round-trips an entry across reloads', async () => {
const c1 = await loadAppInfoCache({ configDir: dir })
await c1.set('http://localhost:9999', 'app-1', metaInfoOnly())
const c2 = await loadAppInfoCache({ configDir: dir })
const got = c2.get('http://localhost:9999', 'app-1')
expect(got).toBeDefined()
expect(got?.meta.info?.id).toBe('app-1')
expect(got?.meta.coveredFields.has(FieldInfo)).toBe(true)
})
it('isFresh respects TTL', async () => {
const now = new Date('2026-05-09T00:00:00Z')
const c = await loadAppInfoCache({ configDir: dir, now: () => now })
await c.set('h', 'app-1', metaInfoOnly())
const r = c.get('h', 'app-1')
expect(r).toBeDefined()
expect(c.isFresh(r!, now)).toBe(true)
expect(c.isFresh(r!, new Date(now.getTime() + APP_INFO_TTL_MS - 1))).toBe(true)
expect(c.isFresh(r!, new Date(now.getTime() + APP_INFO_TTL_MS))).toBe(false)
expect(c.isFresh(r!, new Date(now.getTime() + APP_INFO_TTL_MS + 60_000))).toBe(false)
})
it('keys by (host, app_id) — different hosts isolate', async () => {
const c = await loadAppInfoCache({ configDir: dir })
await c.set('h1', 'app-1', metaInfoOnly())
expect(c.get('h2', 'app-1')).toBeUndefined()
expect(c.get('h1', 'app-1')).toBeDefined()
})
it('delete removes entry from disk', async () => {
const c1 = await loadAppInfoCache({ configDir: dir })
await c1.set('h', 'app-1', metaInfoOnly())
await c1.delete('h', 'app-1')
const c2 = await loadAppInfoCache({ configDir: dir })
expect(c2.get('h', 'app-1')).toBeUndefined()
})
it('writes file with 0600 permission', async () => {
const c = await loadAppInfoCache({ configDir: dir })
await c.set('h', 'app-1', metaInfoOnly())
const { stat } = await import('node:fs/promises')
const s = await stat(cachePath(dir))
if (process.platform !== 'win32')
expect(s.mode & 0o777).toBe(0o600)
})
it('missing cache file is not an error', async () => {
const c = await loadAppInfoCache({ configDir: dir })
expect(c.get('h', 'app-1')).toBeUndefined()
})
it('corrupt cache file is treated as empty', async () => {
const { mkdir, writeFile } = await import('node:fs/promises')
await mkdir(join(dir, 'cache'), { recursive: true })
await writeFile(cachePath(dir), '{not json', 'utf8')
const c = await loadAppInfoCache({ configDir: dir })
expect(c.get('h', 'app-1')).toBeUndefined()
})
it('updates same key in place (no growth)', async () => {
const c = await loadAppInfoCache({ configDir: dir })
await c.set('h', 'app-1', metaInfoOnly())
const slim: AppMeta = {
...metaInfoOnly(),
coveredFields: new Set([FieldInfo, FieldParameters]),
parameters: { opening_statement: 'hi' },
}
await c.set('h', 'app-1', slim)
const raw = await readFile(cachePath(dir), 'utf8')
const parsed = JSON.parse(raw) as { entries: Record<string, unknown> }
expect(Object.keys(parsed.entries)).toHaveLength(1)
})
})