|
|
@@ -1,7 +1,7 @@
|
|
|
-import { mkdtemp } from 'node:fs/promises'
|
|
|
+import { mkdtemp, rm } from 'node:fs/promises'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
import { join } from 'node:path'
|
|
|
-import { describe, expect, it, vi } from 'vitest'
|
|
|
+import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment'
|
|
|
import type { ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment'
|
|
|
import { DeepSeekFileStore, MAX_CHAT_IMAGE_BYTES } from '../src/file-store.ts'
|
|
|
@@ -31,6 +31,12 @@ const CONNECTION = { baseURL: 'https://api.deepseek.com', apiKey: 'key' }
|
|
|
const POLICY = { expiresAfterSeconds: 604_800, refreshMarginSeconds: 3_600, quotaCleanupBatch: 100 }
|
|
|
const NOW = 1_700_000_000_000
|
|
|
|
|
|
+/** Every temp store root created by this file, removed after each test. */
|
|
|
+const roots: string[] = []
|
|
|
+afterEach(async () => {
|
|
|
+ for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
|
|
|
+})
|
|
|
+
|
|
|
function requestUrl(input: string | URL | Request): string {
|
|
|
if (typeof input === 'string') return input
|
|
|
return input instanceof URL ? input.href : input.url
|
|
|
@@ -64,6 +70,7 @@ function uploadFetch(now: () => number = () => NOW) {
|
|
|
describe('DeepSeekFileStore', () => {
|
|
|
it('singleflights the first upload and reuses the durable mapping across store instances', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
const remote = uploadFetch()
|
|
|
const first = new DeepSeekFileStore({ index, now: () => NOW, fetch: remote.fetchImpl })
|
|
|
@@ -84,6 +91,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('keeps a shared upload alive while another waiter remains', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
let complete: ((response: Response) => void) | undefined
|
|
|
let uploadSignal: AbortSignal | undefined
|
|
|
@@ -123,6 +131,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('aborts the shared upload after its only waiter cancels', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
let uploadSignal: AbortSignal | undefined
|
|
|
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => {
|
|
|
@@ -149,6 +158,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('normalizes a non-Error cancellation reason', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => (
|
|
|
new Promise<Response>((_resolve, reject) => {
|
|
|
init?.signal?.addEventListener('abort', () => {
|
|
|
@@ -176,6 +186,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('starts a fresh upload while the cancelled transport is settling', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
let requests = 0
|
|
|
const fetchImpl = vi.fn((_url: string | URL | Request, init?: RequestInit) => {
|
|
|
requests += 1
|
|
|
@@ -222,6 +233,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('does not persist an upload whose response is missing and retries on the next request', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
const good = uploadFetch()
|
|
|
let first = true
|
|
|
@@ -242,6 +254,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('rejects an upload response whose byte count differs from the request version', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const fetchImpl = vi.fn(() => Promise.resolve(new Response(JSON.stringify({
|
|
|
id: 'file-api-wrong-size', object: 'file', bytes: 2, created_at: NOW / 1_000,
|
|
|
filename: 'dsh-wrong.png', purpose: 'user_data',
|
|
|
@@ -262,6 +275,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
['image/gif', 'gif'],
|
|
|
] as const)('uses the %s filename extension for uploads', async (mediaType, extension) => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const remote = uploadFetch()
|
|
|
const store = new DeepSeekFileStore({
|
|
|
index: new DeepSeekUploadIndex(join(dir, `${extension}.json`)),
|
|
|
@@ -279,6 +293,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('normalizes a non-Error failure from the durable upload index', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
vi.spyOn(index, 'get').mockRejectedValue('index unavailable')
|
|
|
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: vi.fn() as typeof fetch })
|
|
|
@@ -291,6 +306,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('reuses local expires_at above the refresh margin and uploads again at the margin', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
let now = NOW
|
|
|
const remote = uploadFetch(() => now)
|
|
|
@@ -311,6 +327,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('releases an indexed file through DELETE and removes only that mapping', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
const remote = uploadFetch()
|
|
|
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: remote.fetchImpl })
|
|
|
@@ -323,6 +340,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('removes a losing upload and keeps the winning durable mapping when duplicate cleanup fails', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
vi.spyOn(index, 'commit').mockResolvedValue({
|
|
|
accepted: false,
|
|
|
@@ -352,6 +370,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('reclaims one owned file after quota rejection and retries the upload once', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
let uploads = 0
|
|
|
const fetchImpl = vi.fn((input: string | URL | Request, init?: RequestInit) => {
|
|
|
if (init?.method === 'POST') {
|
|
|
@@ -394,6 +413,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('preserves a quota error when no harness-owned file can be reclaimed', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
|
|
|
if (init?.method === 'POST') return Promise.resolve(new Response(JSON.stringify({
|
|
|
error: { message: 'file count quota exceeded', code: 'file_quota' },
|
|
|
@@ -418,6 +438,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('finishes pagination before deleting cursor files during quota recovery', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const deleted = new Set<string>()
|
|
|
const fetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
|
const target = new URL(requestUrl(input))
|
|
|
@@ -456,6 +477,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('stops pagination when a page omits or repeats its cursor', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
for (const mode of ['missing', 'repeated'] as const) {
|
|
|
let page = 0
|
|
|
const fetchImpl = vi.fn((input: string | URL | Request, init?: RequestInit) => {
|
|
|
@@ -482,6 +504,7 @@ describe('DeepSeekFileStore', () => {
|
|
|
|
|
|
it('releases every batch and clears the scoped upload index', async () => {
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-file-store-'))
|
|
|
+ roots.push(dir)
|
|
|
const index = new DeepSeekUploadIndex(join(dir, 'index.json'))
|
|
|
const store = new DeepSeekFileStore({ index, now: () => NOW, fetch: vi.fn() as typeof fetch })
|
|
|
const reclaim = vi.spyOn(store, 'reclaimOldestOwned')
|