test(session): align migration corpus and CLI expectations with lazy reads
This commit is contained in:
1 parent
6362113ef8
commit
e6e5d95b29
12 files changed
+58
-40
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-19-session-local-subagent-migration.md
|
||||
2026-09-19-session-local-subagent-migration.md: c3da2812ac2d48fe4894b62b3b5743664564633a
|
||||
2026-09-19-session-local-subagent-migration.zh.md: 43875dd8cd34635d33f711745aa9725641e4cec3
|
||||
2026-09-19-session-local-subagent-migration.md: e02d2434b6f778556db454d88776a97ee699709f
|
||||
2026-09-19-session-local-subagent-migration.zh.md: 61cb891105c39e9ef9524f1c9df2f257d8938045
|
||||
@@ -30,4 +30,6 @@ This partially supersedes runtime catalog prerequisites and corpus revisions in
|
||||
|
||||
Explicit direct-child discovery now enumerates N headers in addition to materializing D catalog facts. It avoids all child-body decoding, while ordinary parent migration has no corpus-dependent preparation. Historical rows may initially show an id instead of a label and cannot promise resumability. Headers that cannot be decoded cannot contribute a navigation row, but do not block opening another known Session.
|
||||
|
||||
The V2 parent/child regression needs two independently stored historical roles to exercise deferred migration and unchanged predecessors. Its additional child raises the retained corpus budget from ten to eleven roles; the gate still rejects a twelfth role.
|
||||
|
||||
Raw and compressed persistence regressions cover V0–V3, child corruption and future formats, independent publication, unchanged predecessors, and stable parent revisions across child mutations. Service tests verify only the parent is observed. Client and Host tests verify unresolved addresses, selected-child decoding, mode resolution, and ownership rejection. The shipped Web preset-migration snapshot round-trips parent and child generations with a corrupt sibling and checks that only explicitly opened Sessions migrate.
|
||||
@@ -30,4 +30,6 @@ Catalog 仍是成功创建事实的持久化来源。未解析发现行既不是
|
||||
|
||||
显式直接子发现除物化 D 条 catalog 事实外,还枚举 N 个头部。它避免所有子正文解码,而普通父迁移没有依赖全库的准备工作。历史行最初可能显示 id 而非标签,且不能承诺可续接。无法解码的头部不能贡献导航行,但不会阻止打开另一个已知会话。
|
||||
|
||||
V2 父子会话回归需要两个独立存储的历史角色,以验证延迟迁移和前代文件保持不变。新增子会话将保留语料预算从十个角色增至十一个;门禁仍拒绝第十二个角色。
|
||||
|
||||
原始与压缩持久化回归覆盖 V0–V3、子损坏与未来格式、独立发布、前代不变,以及子变化期间父版本标识稳定。服务测试确认只观察父会话。Client 和 Host 测试验证未解析地址、选中子解码、模式解析与归属拒绝。正式 Web preset-migration 快照在损坏兄弟会话存在时往返父子代次,并检查只有显式打开的会话才迁移。
|
||||
@@ -58,7 +58,7 @@ function textOf(value: unknown, path: string): string | undefined {
|
||||
function readObject(file: string): Record<string, unknown> {
|
||||
let contents: unknown
|
||||
try {
|
||||
contents = JSON.parse(readFileSync(file, 'utf8')) as unknown
|
||||
contents = JSON.parse(readFileSync(file, 'utf8'))
|
||||
} catch (error) {
|
||||
throw new Error(`${file}: ${String(error)}`)
|
||||
}
|
||||
|
||||
@@ -101,15 +101,27 @@ function renderTab(state: Partial<PluginManagerState> = {}, config: Partial<Conf
|
||||
setRowEnabled: vi.fn(),
|
||||
dismissNotice: vi.fn(),
|
||||
}
|
||||
const props = {
|
||||
const unusedHook = () => { throw new Error('unused standard hook') }
|
||||
const renderSlot: PluginManagerPageProps['renderSlot'] = (name, owner, opts) => {
|
||||
if (!('view' in owner) || (owner.view !== 'summary' && owner.view !== 'page')) {
|
||||
throw new Error('configuration fixture requires a summary or page view')
|
||||
}
|
||||
return bodies[`${name}:${opts?.only ?? opts?.entryKey ?? ''}`]?.(owner.view) ?? null
|
||||
}
|
||||
const props: PluginManagerPageProps = {
|
||||
usePanelInfo: unusedHook,
|
||||
useSessions: unusedHook,
|
||||
useSessionStatus: unusedHook,
|
||||
useSessionRetainInfo: unusedHook,
|
||||
useResource: unusedHook,
|
||||
useWorkspaces: unusedHook,
|
||||
t,
|
||||
resolveText,
|
||||
...actions,
|
||||
usePluginManager: bindSnapshotSelector(store),
|
||||
useConfigLedger: bindSnapshotSelector(ledger),
|
||||
renderSlot: (name: string, owner: { view: 'summary' | 'page' }, opts: { only?: string; entryKey?: string }) =>
|
||||
bodies[`${name}:${opts.only ?? opts.entryKey ?? ''}`]?.(owner.view) ?? null,
|
||||
} as unknown as PluginManagerPageProps
|
||||
renderSlot,
|
||||
}
|
||||
const { rerender } = render(<PluginManagerPage {...props} />)
|
||||
return {
|
||||
store,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @vitest-environment jsdom
|
||||
import type { ReactNode } from 'react'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
ISessions, SessionListState, SessionReference, SessionSnapshot,
|
||||
SessionListState, SessionReference, SessionSnapshot,
|
||||
} from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { ResourceProvider } from '@deepseek-ai/dsh-client-resources/client'
|
||||
import { sessionSnapshot } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
@@ -85,19 +85,22 @@ describe('Sidebar chat registration', () => {
|
||||
let provider: ResourceProvider<'subagentchat'> | undefined
|
||||
let definition: SidebarRightTabDefinition | undefined
|
||||
const registrations: { options: Record<string, unknown>; component: unknown }[] = []
|
||||
const ctx = {
|
||||
sessions: { retain, refresh, list } as unknown as ISessions,
|
||||
resources: { register: (value: ResourceProvider<'subagentchat'>) => { provider = value; return () => {} } },
|
||||
sidebarRightTabs: { register: (value: SidebarRightTabDefinition) => { definition = value; return () => {} } },
|
||||
slots: {
|
||||
inject: (_name: string, install: () => () => void) => install(),
|
||||
register: (options: Record<string, unknown>, component: unknown) => {
|
||||
registrations.push({ options, component })
|
||||
return () => {}
|
||||
},
|
||||
const ctx = new Context()
|
||||
onTestFinished(async () => { await ctx.fiber.dispose() })
|
||||
ctx.provide('sessions', { retain, refresh, list })
|
||||
ctx.provide('resources', {
|
||||
register: (value: ResourceProvider<'subagentchat'>) => { provider = value; return () => {} },
|
||||
})
|
||||
ctx.provide('sidebarRightTabs', {
|
||||
register: (value: SidebarRightTabDefinition) => { definition = value; return () => {} },
|
||||
})
|
||||
ctx.provide('slots', {
|
||||
inject: (_name: string, install: () => () => void) => install(),
|
||||
register: (options: Record<string, unknown>, component: unknown) => {
|
||||
registrations.push({ options, component })
|
||||
return () => {}
|
||||
},
|
||||
effect: (install: () => unknown) => { install(); return () => {} },
|
||||
} as unknown as Context
|
||||
})
|
||||
|
||||
registerSidebarChat(ctx, (key: string) => key === 'sidebar.chat' ? 'Chat' : key)
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('one-time V4 migration command', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['none', 'zstd'] as const)('preserves %s parent/child history and catalog with serial or parallel jobs', async (compression) => {
|
||||
it.each(['none', 'zstd'] as const)('migrates %s parent/child history independently with serial or parallel jobs', async (compression) => {
|
||||
const outputs: Buffer[][] = []
|
||||
for (const jobs of [1, 2]) {
|
||||
const root = temporaryRoot()
|
||||
@@ -246,8 +246,9 @@ describe('one-time V4 migration command', () => {
|
||||
))
|
||||
current.push(decoded)
|
||||
}
|
||||
expect(current[0]!.toString().trim().split('\n').slice(1).map((line): unknown => JSON.parse(line))).toMatchObject([
|
||||
{ type: 'subagent/catalog', data: { childId: 'z-child', childCreatedAt: 2, mode: 'continuable', label: 'saved child' } },
|
||||
expect(current[0]!.toString().trim().split('\n').slice(1)).toEqual([])
|
||||
expect(current[1]!.toString().trim().split('\n').slice(1).map((line): unknown => JSON.parse(line))).toMatchObject([
|
||||
{ type: 'subagent/descriptor', data: { mode: 'continuable', label: 'saved child' } },
|
||||
])
|
||||
outputs.push(current)
|
||||
}
|
||||
@@ -341,7 +342,7 @@ describe('one-time V4 migration command', () => {
|
||||
Backend.prototype.open = async function(id, access, options) {
|
||||
if (id === 'a-parent' && access === 'write' && !changed) {
|
||||
changed = true;
|
||||
throw new JsonlGenerationSourceChangedError(${JSON.stringify(child.path)});
|
||||
throw new JsonlGenerationSourceChangedError(${JSON.stringify(parent.path)});
|
||||
}
|
||||
return open.call(this, id, access, options);
|
||||
};
|
||||
@@ -360,7 +361,8 @@ describe('one-time V4 migration command', () => {
|
||||
expect(lines.filter(line => line.includes('RETRY'))).toHaveLength(1)
|
||||
expect(result.summary).toMatchObject({ totals: { converted: 2, alreadyV4: 0, failed: 0, skipped: 0 }, failureGroups: [] })
|
||||
const target = await readFile(join(parent.directory, 'session.v4.jsonl'), 'utf8')
|
||||
expect(target).toContain('"type":"subagent/catalog"')
|
||||
expect(target.trim().split('\n')).toHaveLength(1)
|
||||
expect(JSON.parse(target)).toMatchObject({ version: 4, id: 'a-parent' })
|
||||
for (const source of [parent, child]) expect(await readFile(source.path)).toEqual(source.bytes)
|
||||
})
|
||||
|
||||
|
||||
@@ -671,9 +671,6 @@
|
||||
"packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx": {
|
||||
"04984a2ce1486250236e638e0197da0a1a840855ed3975c59130a85358c7e5fe": 1
|
||||
},
|
||||
"packages/client/ui-plugin-manager/tests/components.client.spec.tsx": {
|
||||
"624ce7f0e7acb423fcd3937994cd5ca0916ac27b76cb56a7587025f209a14351": 1
|
||||
},
|
||||
"packages/client/ui-primitives/tests/highlight-viewport.client.spec.tsx": {
|
||||
"6061de0a1e01547eb1abf9d4e1b5fa79fda112fa35dda980775491123413dcce": 1
|
||||
},
|
||||
@@ -838,13 +835,11 @@
|
||||
"92d757187bb2dba986b160c2417c7b4a8b199784d441bfa0777d8077774b932b": 1
|
||||
},
|
||||
"packages/client/ui-subagent/tests/sidebar-chat.client.spec.tsx": {
|
||||
"024f8748e8908a495117a23a7673046a871f0d8187f8fe561aceba1269aefa31": 1,
|
||||
"6f46259a9f4093d68b3fc7859c263f21b6318197b74aba368e8904321ab5c4f9": 1,
|
||||
"7eb46f4e5e11cef80bd04f36bf7e9b81b3b3e0811e6a50c8dc0e59b13409ab47": 1,
|
||||
"93043ae394699d6c87db0291ac271884e16d5f5e5f80b8dffd6ec2ccbe0cb774": 1,
|
||||
"9fe5203965e5ff62ab3b914bbcbb5229a5146bf9b12839d99684742579b0f30f": 1,
|
||||
"c3f6bb95ba0791435add9be26269aec3bd5cf7bb8a1ffd88913ff8b08b142ed3": 1,
|
||||
"e9abfbf3920ecd6502b74e5eb04d3a1a40b2519f849f5daa193ab7819184ee8b": 1,
|
||||
"ffb048df32b10bac91f37b55c618701aa0f9d24c5b5504a3d3f953dc8b7cc7fb": 1
|
||||
},
|
||||
"packages/client/ui-theme/tests/apply.client.spec.ts": {
|
||||
|
||||
@@ -111,12 +111,14 @@ describe('recorded-session corpus policy', () => {
|
||||
)
|
||||
|
||||
it('bounds explicitly retained roles and requires a baseline/current majority', () => {
|
||||
expect(assertSnapshotCorpusPolicy([
|
||||
baseline, current,
|
||||
{ ...completeV0, selectedVersions: Array<number>(9).fill(0) }, ...adjacent,
|
||||
]).retainedRoles).toBe(11)
|
||||
expect(() => assertSnapshotCorpusPolicy([
|
||||
baseline,
|
||||
current,
|
||||
{ ...completeV0, selectedVersions: Array<number>(11).fill(0) },
|
||||
...adjacent,
|
||||
])).toThrow(`Session corpus retains ${11 + adjacent.length} historical roles; maximum is 10`)
|
||||
baseline, current,
|
||||
{ ...completeV0, selectedVersions: Array<number>(10).fill(0) }, ...adjacent,
|
||||
])).toThrow('Session corpus retains 12 historical roles; maximum is 11')
|
||||
expect(() => assertSnapshotCorpusPolicy([
|
||||
{ ...baseline, selectedVersions: [3] }, completeV0, ...adjacent,
|
||||
])).toThrow(`Session corpus requires a baseline/current majority; baseline=1, current=0, retained=${1 + adjacent.length}`)
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SnapshotCorpusGenerationSummary {
|
||||
}
|
||||
|
||||
const RETAINED_BASELINE_VERSION = 3
|
||||
const MAX_RETAINED_ROLES = 10
|
||||
const MAX_RETAINED_ROLES = 11
|
||||
const REQUIRED_V0_COVERAGE = new Set([
|
||||
'multi-hop',
|
||||
'packed-row',
|
||||
|
||||
@@ -204,7 +204,7 @@ it('keeps V3 replay input plus bounded declared historical migration coverage',
|
||||
const summary = assertSnapshotCorpusPolicy(inventory)
|
||||
expect(summary.baselineRoles).toBeGreaterThan(0)
|
||||
expect(summary).toMatchObject({
|
||||
retainedRoles: 10,
|
||||
retainedRoles: 11,
|
||||
retainedScenarios: 8,
|
||||
})
|
||||
})
|
||||
@@ -22,7 +22,7 @@ function sourceJsonFiles(dir: string): SourceJson[] {
|
||||
const file = path.replaceAll('\\', '/')
|
||||
let contents: unknown
|
||||
try {
|
||||
contents = JSON.parse(readFileSync(join(dir, file), 'utf8')) as unknown
|
||||
contents = JSON.parse(readFileSync(join(dir, file), 'utf8'))
|
||||
} catch (_error) {
|
||||
return { file, metadata: false, invalid: true }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user