Download packages/migration-legacy/test/integration.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/migration-legacy/test/integration.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/migration-legacy/test/integration.test.ts
-
curl -L -o integration.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/migration-legacy/test/integration.test.ts
10.9 kB
| import { describe, expect, it, beforeEach, afterEach } from 'vitest'; | |
| import { mkdir, mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'; | |
| import { tmpdir } from 'node:os'; | |
| import { join } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { detectMigration, runMigration } from '../src/index.js'; | |
| import { computeWorkdirBucket, oldMd5BucketName } from '../src/sessions/workdir-bucket.js'; | |
| const FIXTURES = fileURLToPath(new URL('./fixtures', import.meta.url)); | |
| const SOURCE_HOME = join(FIXTURES, 'multi-workdir', '.kimi'); | |
| const MARKER_PATH = join(SOURCE_HOME, '.migrated-to-kimi-code'); | |
| const FIXTURE_CONFIG = join(SOURCE_HOME, 'config.toml'); | |
| let tgt: string; | |
| beforeEach(async () => { | |
| tgt = await mkdtemp(join(tmpdir(), 'integration-')); | |
| // Clean any leftover artifacts that previous failed runs might have left in | |
| // the committed fixture directory. | |
| await rm(MARKER_PATH, { recursive: true, force: true }); | |
| await rm(FIXTURE_CONFIG, { force: true }); | |
| }); | |
| afterEach(async () => { | |
| await rm(tgt, { recursive: true, force: true }); | |
| await rm(MARKER_PATH, { recursive: true, force: true }); | |
| await rm(FIXTURE_CONFIG, { force: true }); | |
| }); | |
| describe('runMigration (end-to-end on multi-workdir fixture)', () => { | |
| it('migrates everything when scope is full and limit is null', async () => { | |
| const plan = await detectMigration({ sourcePath: SOURCE_HOME }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { | |
| config: true, | |
| mcp: true, | |
| userHistory: true, | |
| skills: true, | |
| sessions: true, | |
| }, | |
| source: SOURCE_HOME, | |
| target: tgt, | |
| }); | |
| expect(report.summary.sessions.sessionsMigrated).toBeGreaterThan(0); | |
| const indexText = await readFile(join(tgt, 'session_index.jsonl'), 'utf-8'); | |
| const indexLines = indexText.split('\n').filter((l) => l.length > 0); | |
| expect(indexLines.length).toBeGreaterThan(0); | |
| const markerText = await readFile(MARKER_PATH, 'utf-8'); | |
| const marker: unknown = JSON.parse(markerText); | |
| expect((marker as { version: number }).version).toBe(1); | |
| }); | |
| it('completes the migration even when the source marker cannot be written', async () => { | |
| // Block the marker path with a directory so writeMarker()'s writeFile fails. | |
| await mkdir(MARKER_PATH, { recursive: true }); | |
| const plan = await detectMigration({ sourcePath: SOURCE_HOME }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: true }, | |
| source: SOURCE_HOME, | |
| target: tgt, | |
| }); | |
| // All data migrated — a completed run must return a report, not reject. | |
| expect(report.summary.sessions.sessionsMigrated).toBeGreaterThan(0); | |
| }); | |
| it('does not write a completed marker when legacy session data remains unreadable', async () => { | |
| const src = await mkdtemp(join(tmpdir(), 'failed-session-marker-src-')); | |
| try { | |
| const bucket = join(src, 'sessions', '11111111111111111111111111111111'); | |
| await mkdir(join(bucket, 'legacy-session'), { recursive: true }); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: true }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.sessions.sessionsFailed).toHaveLength(1); | |
| await expect(readFile(join(src, '.migrated-to-kimi-code'), 'utf-8')).rejects.toThrow(); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('config-only scope writes config but skips sessions', async () => { | |
| // Materialize a config.toml in the fixture; afterEach cleans it up. | |
| await writeFile(FIXTURE_CONFIG, 'default_thinking = true\n'); | |
| const plan = await detectMigration({ sourcePath: SOURCE_HOME }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { | |
| config: true, | |
| mcp: true, | |
| userHistory: true, | |
| skills: true, | |
| sessions: false, | |
| }, | |
| source: SOURCE_HOME, | |
| target: tgt, | |
| }); | |
| expect(report.summary.sessions.scope).toBe('config-only'); | |
| expect(report.summary.sessions.sessionsMigrated).toBe(0); | |
| }); | |
| it('migrates user skills bundles end-to-end and surfaces them in the report', async () => { | |
| // Drive the full pipeline against a synthetic source so the assertion | |
| // tests integration (run-migration wiring + paths + summary plumbing), | |
| // not just the step in isolation. | |
| const src = await mkdtemp(join(tmpdir(), 'skills-e2e-src-')); | |
| try { | |
| await mkdir(join(src, 'skills', 'my-bundle'), { recursive: true }); | |
| await writeFile( | |
| join(src, 'skills', 'my-bundle', 'SKILL.md'), | |
| '---\nname: my-bundle\ndescription: e2e\n---\n', | |
| ); | |
| await writeFile(join(src, 'skills', 'flat.md'), '---\nname: flat\ndescription: e2e\n---\n'); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: false }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.skills.copied).toBe(2); | |
| expect(report.summary.skills.skippedExisting).toBe(0); | |
| await expect( | |
| readFile(join(tgt, 'skills', 'my-bundle', 'SKILL.md'), 'utf-8'), | |
| ).resolves.toContain('my-bundle'); | |
| await expect(readFile(join(tgt, 'skills', 'flat.md'), 'utf-8')).resolves.toContain('flat'); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('skips skills migration when scope.skills is false', async () => { | |
| const src = await mkdtemp(join(tmpdir(), 'skills-off-src-')); | |
| try { | |
| await mkdir(join(src, 'skills', 'mine'), { recursive: true }); | |
| await writeFile(join(src, 'skills', 'mine', 'SKILL.md'), 'x'); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: false, sessions: false }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.skills).toEqual({ copied: 0, skippedExisting: 0 }); | |
| await expect(readFile(join(tgt, 'skills', 'mine', 'SKILL.md'))).rejects.toThrow(); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('does not write a completed marker when the legacy config cannot be parsed', async () => { | |
| const src = await mkdtemp(join(tmpdir(), 'bad-config-marker-src-')); | |
| try { | |
| await writeFile(join(src, 'config.toml'), 'this is = = not valid toml [[['); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| expect(plan.hasConfig).toBe(true); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: true }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.config.sourceUnreadable).toBe(true); | |
| await expect(readFile(join(src, '.migrated-to-kimi-code'), 'utf-8')).rejects.toThrow(); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('does not write a completed marker when the legacy mcp.json cannot be parsed', async () => { | |
| const src = await mkdtemp(join(tmpdir(), 'bad-mcp-marker-src-')); | |
| try { | |
| await writeFile(join(src, 'mcp.json'), 'not json {{{'); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: true }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.mcp.sourceUnreadable).toBe(true); | |
| await expect(readFile(join(src, '.migrated-to-kimi-code'), 'utf-8')).rejects.toThrow(); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('does not write a completed marker when a session target is occupied by a foreign session', async () => { | |
| const src = await mkdtemp(join(tmpdir(), 'conflict-marker-src-')); | |
| try { | |
| const workdir = '/workspace/conflict-proj'; | |
| const uuid = 'conflict-session'; | |
| const bucket = join(src, 'sessions', oldMd5BucketName(workdir)); | |
| await mkdir(join(bucket, uuid), { recursive: true }); | |
| await writeFile(join(src, 'kimi.json'), JSON.stringify({ work_dirs: [{ path: workdir }] })); | |
| await writeFile(join(bucket, uuid, 'context.jsonl'), '{"role":"user","content":"hi"}\n'); | |
| const foreignDir = join( | |
| tgt, | |
| 'sessions', | |
| computeWorkdirBucket(workdir), | |
| `ses_${uuid}`, | |
| ); | |
| await mkdir(foreignDir, { recursive: true }); | |
| await writeFile(join(foreignDir, 'state.json'), '{}'); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: true }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| expect(report.summary.sessions.sessionsConflicts).toHaveLength(1); | |
| await expect(readFile(join(src, '.migrated-to-kimi-code'), 'utf-8')).rejects.toThrow(); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| it('does not copy OAuth credentials into the target', async () => { | |
| // OAuth refresh tokens rotate server-side: they are single-use and | |
| // single-owner. Copying a credential to a second install breaks login | |
| // for whichever side refreshes second. The migration must NOT copy | |
| // credentials — it leaves the legacy login alone and asks the user to | |
| // run /login in kimi-code instead. | |
| const src = await mkdtemp(join(tmpdir(), 'oauth-src-')); | |
| try { | |
| await mkdir(join(src, 'credentials'), { recursive: true }); | |
| await writeFile( | |
| join(src, 'credentials', 'kimi-code.json'), | |
| JSON.stringify({ | |
| access_token: 'a', | |
| refresh_token: 'r', | |
| expires_at: 1, | |
| scope: 's', | |
| token_type: 'Bearer', | |
| }), | |
| ); | |
| const plan = await detectMigration({ sourcePath: src }); | |
| const report = await runMigration({ | |
| plan, | |
| scope: { config: true, mcp: true, userHistory: true, skills: true, sessions: false }, | |
| source: src, | |
| target: tgt, | |
| }); | |
| // The credential must not be copied into the target. | |
| await expect( | |
| readFile(join(tgt, 'credentials', 'kimi-code.json'), 'utf-8'), | |
| ).rejects.toThrow(); | |
| // The report tells the user to sign in again in kimi-code. | |
| expect(report.notices.oauthLoginsRequiringRelogin).toContain('kimi-code'); | |
| } finally { | |
| await rm(src, { recursive: true, force: true }); | |
| } | |
| }); | |
| }); | |