// @vitest-environment node import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import ts from 'typescript'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { listProductCodingSkills } from '@electron/coding-projects/skill-registry'; const PLUGIN_SKILL_ROOT = path.resolve('resources/coding-plugins/data-service/skills/data-service'); const ASSET_ROOT = path.join(PLUGIN_SKILL_ROOT, 'assets'); const ASSETS = ['makelore-data.ts', 'makelore-data.js'] as const; const temporaryRoots: string[] = []; let moduleCounter = 0; afterEach(async () => { vi.restoreAllMocks(); vi.unstubAllGlobals(); delete (globalThis as { __MAKELORE_DATA__?: unknown }).__MAKELORE_DATA__; await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); async function loadSdk(assetName: (typeof ASSETS)[number]): Promise<{ data: { get(collection: string, documentId: string): Promise; list(collection: string, options?: { limit?: number; cursor?: string }): Promise; put(collection: string, documentId: string, data: Record, options?: { ifRevision?: number }): Promise; delete(collection: string, documentId: string, options?: { ifRevision?: number }): Promise; add(collection: string, data: Record): Promise; }; DataServiceError: new (...args: unknown[]) => Error; }> { const source = await readFile(path.join(ASSET_ROOT, assetName), 'utf8'); const root = await mkdtemp(path.join(tmpdir(), 'makelore-data-sdk-')); temporaryRoots.push(root); const moduleName = `${path.basename(assetName, path.extname(assetName))}-${moduleCounter += 1}.mjs`; const javascript = assetName.endsWith('.ts') ? ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, fileName: assetName, }).outputText : source; const modulePath = path.join(root, moduleName); await writeFile(modulePath, javascript, 'utf8'); return await import(`${pathToFileURL(modulePath).href}?v=${moduleCounter}`) as { data: { get(collection: string, documentId: string): Promise; list(collection: string, options?: { limit?: number; cursor?: string }): Promise; put(collection: string, documentId: string, data: Record, options?: { ifRevision?: number }): Promise; delete(collection: string, documentId: string, options?: { ifRevision?: number }): Promise; add(collection: string, data: Record): Promise; }; DataServiceError: new (...args: unknown[]) => Error; }; } function injectRuntime(overrides: Record = {}): void { Object.defineProperty(globalThis, '__MAKELORE_DATA__', { configurable: true, value: { endpoint: 'http://127.0.0.1:4173/api/runtime/data/v1', token: 'preview-token', contractVersion: 1, ...overrides, }, }); } function response(payload: unknown, status = 200, headers: Record = {}): Response { return new Response(payload === undefined ? null : JSON.stringify(payload), { status, headers: { 'content-type': 'application/json', ...headers }, }); } const documentDto = { id: 'todo.1', data: { title: 'Ship P0', done: false }, revision: 7, created_at: '2026-08-26T00:00:00.000Z', updated_at: '2026-08-26T00:00:00.000Z', }; // Skill installation is agent-driven; this fixture keeps its direct-text policy // executable without adding a second runtime installer or template abstraction. async function installFixtureAsset( projectRoot: string, target: string, source: string, ): Promise<'installed' | 'noop' | 'conflict'> { const targetPath = path.join(projectRoot, target); let existing: string | undefined; try { existing = await readFile(targetPath, 'utf8'); } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } if (existing === source) return 'noop'; if (existing !== undefined) return 'conflict'; await mkdir(path.dirname(targetPath), { recursive: true }); await writeFile(targetPath, source, 'utf8'); return 'installed'; } describe.each(ASSETS)('generated Data Service SDK (%s)', (assetName) => { it('returns runtime_unavailable without a network call for absent or invalid injection', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const sdk = await loadSdk(assetName); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'runtime_unavailable', status: 503, retryable: true, }); expect(fetchMock).not.toHaveBeenCalled(); injectRuntime({ contractVersion: 2 }); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'runtime_unavailable', status: 503, }); expect(fetchMock).not.toHaveBeenCalled(); injectRuntime({ endpoint: 'https://cloud.example/api/runtime/data/v1' }); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'runtime_unavailable', status: 503, }); expect(fetchMock).not.toHaveBeenCalled(); }); it('uses the injected local capability, encoded paths, direct DTOs, and one strong If-Match', async () => { injectRuntime(); const fetchMock = vi.fn() .mockResolvedValueOnce(response(documentDto)) .mockResolvedValueOnce(response({ items: [documentDto], next_cursor: null, limit: 50 })) .mockResolvedValueOnce(response(documentDto)) .mockResolvedValueOnce(response(undefined, 204)); vi.stubGlobal('fetch', fetchMock); const sdk = await loadSdk(assetName); await expect(sdk.data.get('todos', 'todo.1')).resolves.toEqual(documentDto); await expect(sdk.data.list('todos', { limit: 50, cursor: 'next cursor' })).resolves.toEqual({ items: [documentDto], next_cursor: null, limit: 50, }); await expect(sdk.data.put('todos', 'todo.1', { title: 'Ship P0' }, { ifRevision: 7 })) .resolves.toEqual(documentDto); await expect(sdk.data.delete('todos', 'todo.1', { ifRevision: 7 })).resolves.toBeUndefined(); expect(fetchMock).toHaveBeenCalledTimes(4); expect(fetchMock.mock.calls[0][0]).toBe( 'http://127.0.0.1:4173/api/runtime/data/v1/collections/todos/documents/todo.1', ); expect(fetchMock.mock.calls[1][0]).toContain('/collections/todos/documents?'); expect(fetchMock.mock.calls[1][0]).toContain('cursor=next+cursor'); expect(fetchMock.mock.calls[2][1]).toMatchObject({ method: 'PUT', headers: expect.objectContaining({ Authorization: 'Bearer preview-token', 'If-Match': '"7"', }), body: JSON.stringify({ data: { title: 'Ship P0' } }), }); expect(fetchMock.mock.calls[3][1]).toMatchObject({ method: 'DELETE', headers: expect.objectContaining({ Authorization: 'Bearer preview-token', 'If-Match': '"7"' }), }); expect(fetchMock.mock.calls[3][1]).not.toHaveProperty('body'); expect(JSON.stringify(fetchMock.mock.calls[3][1])).not.toContain('confirmed'); }); it('strictly projects safe errors and never retries a failed request', async () => { injectRuntime(); const fetchMock = vi.fn() .mockResolvedValueOnce(response({ detail: { code: 'revision_conflict', message: 'secret upstream detail', retryable: false, context: { current_revision: 8 }, }, }, 409, { 'retry-after': '10' })) .mockRejectedValueOnce(new Error('socket detail should not escape')); vi.stubGlobal('fetch', fetchMock); const sdk = await loadSdk(assetName); const conflict = await sdk.data.put('todos', 'todo.1', { title: 'Ship P0' }, { ifRevision: 7 }) .catch((error: unknown) => error as { code: string; status: number; retryable: boolean; retryAfterSeconds?: number; context?: Record; }); expect(conflict).toMatchObject({ code: 'revision_conflict', status: 409, retryable: false, retryAfterSeconds: 10, context: { current_revision: 8 }, }); expect(Object.isFrozen(conflict)).toBe(true); expect(Object.isFrozen(conflict.context!)).toBe(true); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'runtime_unavailable', status: 503, retryable: true, }); expect(fetchMock).toHaveBeenCalledTimes(2); expect(JSON.stringify(await Promise.resolve(fetchMock.mock.calls))).not.toContain('secret upstream detail'); }); it('rejects malformed DTOs and unknown error codes without exposing upstream detail', async () => { injectRuntime(); const fetchMock = vi.fn() .mockResolvedValueOnce(response({ ...documentDto, extra: 'not allowed' })) .mockResolvedValueOnce(response({ detail: { code: 'toString', message: 'should not be accepted', retryable: false, } }, 502)); vi.stubGlobal('fetch', fetchMock); const sdk = await loadSdk(assetName); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'upstream_invalid_response', status: 502, }); await expect(sdk.data.get('todos', 'todo.1')).rejects.toMatchObject({ code: 'upstream_invalid_response', status: 502, }); expect(JSON.stringify(fetchMock.mock.calls)).not.toContain('should not be accepted'); }); it('keeps DELETE as a document operation and add creates a local UUID before put', async () => { injectRuntime(); const fetchMock = vi.fn().mockResolvedValue(response(documentDto)); vi.stubGlobal('fetch', fetchMock); const uuid = '11111111-1111-4111-8111-111111111111'; vi.stubGlobal('crypto', { randomUUID: vi.fn(() => uuid) }); const sdk = await loadSdk(assetName); await expect(sdk.data.add('todos', { title: 'Ship P0' })).resolves.toEqual(documentDto); expect(fetchMock.mock.calls[0][0]).toContain(`/documents/${uuid}`); }); }); describe('bundled Data Service Skill packaging', () => { it('ships both canonical assets and an ordered workflow with explicit completion criteria', async () => { const skill = await readFile(path.join(PLUGIN_SKILL_ROOT, 'SKILL.md'), 'utf8'); const readme = await readFile(path.resolve('README.md'), 'utf8'); const tsAsset = await readFile(path.join(ASSET_ROOT, 'makelore-data.ts'), 'utf8'); const jsAsset = await readFile(path.join(ASSET_ROOT, 'makelore-data.js'), 'utf8'); expect(skill).toMatch(/^---\nname: data-service\n/m); expect(skill.indexOf('inspect')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('explicit')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('data_service_configure')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('makelore-data.ts')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('agent_browser')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('read-back')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('report')).toBeGreaterThanOrEqual(0); expect(skill.indexOf('inspect')).toBeLessThan(skill.indexOf('data_service_configure')); expect(skill.indexOf('data_service_configure')).toBeLessThan(skill.indexOf('agent_browser')); expect(skill.indexOf('agent_browser')).toBeLessThan(skill.indexOf('read-back')); expect(skill.indexOf('read-back')).toBeLessThan(skill.indexOf('report')); expect(skill).toContain('src/lib/makelore-data.js'); expect(skill).toContain('makelore-data.js'); expect(skill).toContain('injectProjectData: true'); expect(skill).not.toContain('inject_project_data'); expect(skill).toContain('逐字复制'); expect(skill).toContain('no-op'); expect(skill).toContain('冲突'); expect(readme).toContain('data-service'); expect(readme).toContain('显式请求'); expect(readme).toContain('预览'); const packaged = await listProductCodingSkills( path.resolve('resources/coding-skills'), [], [{ id: 'data-service', directory: PLUGIN_SKILL_ROOT }], ); const dataService = packaged.find(({ id }) => id === 'data-service'); expect(dataService).toMatchObject({ id: 'data-service', name: 'data-service', location: PLUGIN_SKILL_ROOT, }); expect(dataService?.entries).toEqual(expect.arrayContaining([ { path: 'SKILL.md', type: 'file' }, { path: 'assets', type: 'directory' }, { path: 'assets/makelore-data.js', type: 'file' }, { path: 'assets/makelore-data.ts', type: 'file' }, ])); for (const source of [tsAsset, jsAsset]) { expect(source).toContain('globalThis.__MAKELORE_DATA__'); expect(source).toContain('encodeURIComponent'); expect(source).not.toMatch(/firebase|works square|https?:\/\/[^'"`$]*api/i); expect(source).not.toContain('confirmed'); expect(source).not.toMatch(/retry\s*\(/i); } }); it.each([ { label: 'src TypeScript', target: 'src/lib/makelore-data.ts', asset: 'makelore-data.ts', }, { label: 'src JavaScript', target: 'src/lib/makelore-data.js', asset: 'makelore-data.js', }, { label: 'root TypeScript', target: 'makelore-data.ts', asset: 'makelore-data.ts', }, ])('keeps first/repeat/conflict install semantics for $label fixtures', async ({ target, asset }) => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-data-install-')); temporaryRoots.push(root); const source = await readFile(path.join(ASSET_ROOT, asset), 'utf8'); await expect(installFixtureAsset(root, target, source)).resolves.toBe('installed'); await expect(readFile(path.join(root, target), 'utf8')).resolves.toBe(source); await expect(installFixtureAsset(root, target, source)).resolves.toBe('noop'); await writeFile(path.join(root, target), `${source}\n// local edit\n`, 'utf8'); await expect(installFixtureAsset(root, target, source)).resolves.toBe('conflict'); await expect(readFile(path.join(root, target), 'utf8')).resolves.toContain('// local edit'); }); });