Skip to content

Commit ea2fd08

Browse files
committed
fix(migration): Separate report counts by item type
- Show sessions, workspaces, and assistant directories separately. - Exclude routine auxiliary paths from displayed skip counts. - Derive historical counts from saved manifests without rerunning migration, and explicitly report unavailable breakdowns. - Preserve actual warnings and distinguish staged from imported data. - Add localized labels and regression coverage.
1 parent bb748c6 commit ea2fd08

10 files changed

Lines changed: 153 additions & 9 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'node:test';
3+
import { reportRows } from '../src/apps/data-migrator/ui/report.mjs';
4+
5+
const text = { sessions: 'Sessions', workspaces: 'Workspaces', assistantDirectories: 'Assistants', imported: 'imported', staged: 'staged', skipped: 'skipped', itemCountsUnavailable: 'unavailable' };
6+
const report = { domainResults: [{ domain: 'workspace_sessions', state: 'verified', imported: 175, skipped: 173, warnings: [{ severity: 'info', code: 'session_path_not_migrated' }] }] };
7+
const counts = { sessions: { imported: 143, skipped: 0 }, workspaces: { imported: 30, skipped: 0 }, assistantDirectories: { imported: 2, skipped: 0 } };
8+
test('renders separate entity counts and hides auxiliary exclusions', () => {
9+
assert.deepEqual(reportRows(report, counts, text), [
10+
['Sessions', '143 imported, 0 skipped'], ['Workspaces', '30 imported, 0 skipped'], ['Assistants', '2 imported, 0 skipped'],
11+
]);
12+
});
13+
test('retains real warnings and does not label staged data imported', () => {
14+
const staged = structuredClone(report);
15+
staged.domainResults[0].state = 'failed';
16+
staged.domainResults[0].warnings.push({ severity: 'warning', code: 'session_source_skipped' });
17+
const rows = reportRows(staged, counts, text);
18+
assert.equal(rows[0][1], '143 staged, 0 skipped');
19+
assert.deepEqual(rows.at(-1), ['workspace_sessions', 'session_source_skipped']);
20+
});
21+
test('missing historical manifest is explicit instead of inventing zero counts', () => {
22+
assert.deepEqual(reportRows(report, null, text), [['Sessions', 'unavailable'], ['Workspaces', 'unavailable'], ['Assistants', 'unavailable']]);
23+
});

‎src/apps/data-migrator/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cargo test -p openbitfun-data-migrator -p openbitfun-legacy-migration-adapters -
3434
cargo test -p openbitfun-legacy-migration --test migration_engine_contracts
3535
node --test scripts/data-migrator-tauri-build.test.mjs
3636
node --check src/apps/data-migrator/ui/app.js
37+
node --test scripts/data-migrator-report.test.mjs
3738
pnpm run theme:color-audit:all
3839
```
3940

‎src/apps/data-migrator/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ needed for migration. ARM Windows/Linux packages are not currently produced.
3535
5. Read the report. Sign in again or repair paths where indicated, close the
3636
tool, and open OpenBitFun yourself.
3737

38+
The report lists Sessions, workspace records, and assistant directories separately.
39+
Expected auxiliary exclusions (such as request traces and lock files) do not
40+
contribute to these displayed counts. Historical runs use their saved manifests
41+
for the same breakdown; missing manifests are shown as unavailable.
42+
3843
The UI uses the shared design-system tokens bundled offline, follows the system
3944
light/dark/high-contrast setting, and offers English, Simplified Chinese, and
4045
Traditional Chinese.

‎src/apps/data-migrator/src/app_state.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub(crate) struct MigratorView {
167167
pub findings: Vec<ScanFinding>,
168168
pub plan: Option<MigrationPlan>,
169169
pub report: Option<MigrationRunReport>,
170+
pub workspace_counts: Option<openbitfun_legacy_migration_adapters::WorkspaceReportCounts>,
170171
pub progress: Option<MigrationProgressEvent>,
171172
pub blockers: Vec<WriterProcess>,
172173
pub status: MigrationRunStatus,
@@ -794,6 +795,9 @@ fn validate_selection(selection: &MigrationSelection) -> Result<(), CommandError
794795
fn snapshot(session: &MigratorSession) -> MigratorView {
795796
let plan = session.plan.as_ref().map(redact_plan_for_ui);
796797
let report = session.report.as_ref().map(redact_report_for_ui);
798+
let workspace_counts = session.report.as_ref().and_then(|report| {
799+
openbitfun_legacy_migration_adapters::workspace_report_counts(&session.roots, report).ok()
800+
});
797801
MigratorView {
798802
tool_version: env!("CARGO_PKG_VERSION").to_string(),
799803
locations: session.roots.clone(),
@@ -818,6 +822,7 @@ fn snapshot(session: &MigratorSession) -> MigratorView {
818822
),
819823
plan,
820824
report,
825+
workspace_counts,
821826
progress: session.progress.clone(),
822827
blockers: session.blockers.clone(),
823828
status: session.status,

‎src/apps/data-migrator/ui/app.js‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { invoke } from './transport.js';
2+
import { reportRows } from './report.mjs';
23

34
async function loadTranslations() {
45
try {
@@ -98,10 +99,6 @@ function row(title, detail) {
9899
return item;
99100
}
100101

101-
function transferLabel(result) {
102-
return result.state === 'verified' ? text.imported : text.staged;
103-
}
104-
105102
function groupLabel(id) {
106103
const labels = groups.find(([group]) => group === id)?.[1];
107104
return (labels?.[locale] || labels?.en || [id])[0];
@@ -202,11 +199,8 @@ function render(view) {
202199

203200
const reportSummary = document.getElementById('report-summary');
204201
if (view.report) {
205-
reportSummary.replaceChildren(...view.report.domainResults.flatMap((result) => [
206-
row(result.domain, `${result.imported} ${transferLabel(result)}, ${result.skipped} ${text.skipped}, ${result.warnings.filter((item) => item.severity !== 'info').length} ${text.warnings}`),
207-
...[...new Set(result.warnings.filter((item) => item.severity !== 'info').map((item) => item.code))]
208-
.map((code) => row(result.domain, text[code] || code)),
209-
]));
202+
reportSummary.replaceChildren(...reportRows(view.report, view.workspaceCounts, text)
203+
.map(([title, detail]) => row(title, detail)));
210204
}
211205
show('report-card', !view.running && (Boolean(view.report) || view.status === 'cancelled'));
212206
const canExportDiagnostics = ['failed_recoverable', 'failed_manual_action_required'].includes(view.status);

‎src/apps/data-migrator/ui/locales.json‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"en": {
3+
"sessions": "Sessions",
4+
"workspaces": "Workspaces",
5+
"assistantDirectories": "Assistant directories",
6+
"itemCountsUnavailable": "Item counts are unavailable for this run.",
37
"eyebrow": "OpenBitFun Data Migrator",
48
"title": "Import data from BitFun",
59
"intro": "A separate, offline tool. Choose what to import; your original BitFun data is preserved.",
@@ -76,6 +80,10 @@
7680
"extension_item_skipped": "Incomplete or unsupported extensions will be skipped; valid extensions can continue."
7781
},
7882
"zh-CN": {
83+
"sessions": "会话",
84+
"workspaces": "工作区",
85+
"assistantDirectories": "助手目录",
86+
"itemCountsUnavailable": "本次迁移的分类计数不可用。",
7987
"eyebrow": "OpenBitFun 独立数据迁移器",
8088
"title": "从 BitFun 导入数据",
8189
"intro": "独立运行,离线导入。选择需要迁移的内容,原始 BitFun 数据会被保留。",
@@ -152,6 +160,10 @@
152160
"extension_item_skipped": "将跳过不完整或不支持的扩展,其他有效扩展可继续迁移。"
153161
},
154162
"zh-TW": {
163+
"sessions": "工作階段",
164+
"workspaces": "工作區",
165+
"assistantDirectories": "助理目錄",
166+
"itemCountsUnavailable": "本次遷移的分類計數無法取得。",
155167
"eyebrow": "OpenBitFun 獨立資料遷移器",
156168
"title": "從 BitFun 匯入資料",
157169
"intro": "獨立執行,離線匯入。選擇需要遷移的內容,原始 BitFun 資料會被保留。",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function reportRows(report, workspaceCounts, text) {
2+
return report.domainResults.flatMap((result) => {
3+
const transferred = result.state === 'verified' ? text.imported : text.staged;
4+
const warnings = result.warnings.filter((item) => item.severity !== 'info');
5+
const rows = result.domain === 'workspace_sessions'
6+
? [
7+
['sessions', text.sessions],
8+
['workspaces', text.workspaces],
9+
['assistantDirectories', text.assistantDirectories],
10+
].map(([key, label]) => {
11+
const counts = workspaceCounts?.[key];
12+
return [label, counts
13+
? `${counts.imported} ${transferred}, ${counts.skipped} ${text.skipped}`
14+
: text.itemCountsUnavailable];
15+
})
16+
: [[result.domain, `${result.imported} ${transferred}, ${result.skipped} ${text.skipped}, ${warnings.length} ${text.warnings}`]];
17+
rows.push(...[...new Set(warnings.map((item) => item.code))]
18+
.map((code) => [result.domain, text[code] || code]));
19+
return rows;
20+
});
21+
}

‎src/crates/services/legacy-migration-adapters/src/agent_coordination.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,11 @@ mod tests {
14781478
.join(COORDINATION_RELATIVE_PATH)
14791479
.is_file());
14801480
assert!(deep.is_dir());
1481+
let counts = crate::workspace_report_counts(&roots, &report).unwrap();
1482+
assert_eq!(counts.sessions.imported, 2);
1483+
assert_eq!(counts.sessions.skipped, 0);
1484+
assert_eq!(counts.workspaces.imported, 1);
1485+
assert_eq!(counts.assistant_directories.imported, 1);
14811486
let relative = deep.strip_prefix(&roots.legacy_home_root).unwrap();
14821487
assert_eq!(
14831488
fs::read(roots.target_home_root.join(relative).join("content.txt")).unwrap(),

‎src/crates/services/legacy-migration-adapters/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod remote_connect;
1212
mod remote_ssh;
1313
mod settings;
1414
mod workspace_sessions;
15+
pub use workspace_sessions::{workspace_report_counts, MigrationItemCounts, WorkspaceReportCounts};
1516

1617
use openbitfun_legacy_migration::{
1718
DomainContext, DomainScan, LegacyDomainAdapter, LegacyMigrationResult, MigrationRoots,

‎src/crates/services/legacy-migration-adapters/src/workspace_sessions.rs‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,83 @@ const SESSION_OWNED_DIRECTORIES: &[&str] = &["snapshots", "artifacts", "tool-res
5454

5555
pub(crate) struct WorkspaceSessionsAdapter;
5656

57+
#[derive(Debug, Clone, Default, Serialize)]
58+
pub struct MigrationItemCounts {
59+
pub imported: u64,
60+
pub skipped: u64,
61+
}
62+
63+
#[derive(Debug, Clone, Default, Serialize)]
64+
#[serde(rename_all = "camelCase")]
65+
pub struct WorkspaceReportCounts {
66+
pub sessions: MigrationItemCounts,
67+
pub workspaces: MigrationItemCounts,
68+
pub assistant_directories: MigrationItemCounts,
69+
}
70+
71+
/// Project both old and new run manifests into user-facing entity counts.
72+
/// Auxiliary exclusions remain in the original manifest, not in these totals.
73+
pub fn workspace_report_counts(
74+
roots: &MigrationRoots,
75+
report: &openbitfun_product_domains::legacy_migration::MigrationRunReport,
76+
) -> LegacyMigrationResult<WorkspaceReportCounts> {
77+
uuid::Uuid::parse_str(&report.run_id)
78+
.map_err(|_| LegacyMigrationError::InvalidRequest("invalid migration run id".into()))?;
79+
let layout = openbitfun_legacy_migration::MigrationLayout::new(roots, &report.run_id);
80+
let root = layout.stage_root().join("workspace-sessions");
81+
let manifest: WorkspaceSessionsManifest =
82+
read_bounded_json(&root, &root.join("manifest.json"))?;
83+
let plan: openbitfun_product_domains::legacy_migration::MigrationPlan = layout
84+
.read_json(&layout.plan_path())?
85+
.ok_or_else(|| LegacyMigrationError::InvalidPlan("migration plan is missing".into()))?;
86+
let counts = |actions: Vec<SessionImportAction>| MigrationItemCounts {
87+
imported: actions
88+
.iter()
89+
.filter(|a| **a == SessionImportAction::Import)
90+
.count() as u64,
91+
skipped: actions
92+
.iter()
93+
.filter(|a| **a != SessionImportAction::Import)
94+
.count() as u64,
95+
};
96+
let mut result = WorkspaceReportCounts {
97+
sessions: counts(manifest.sessions.iter().map(|e| e.action).collect()),
98+
workspaces: MigrationItemCounts {
99+
imported: manifest.workspace_id_map.len() as u64,
100+
skipped: 0,
101+
},
102+
assistant_directories: counts(
103+
manifest
104+
.assistant_workspaces
105+
.iter()
106+
.map(|e| e.action)
107+
.collect(),
108+
),
109+
};
110+
let target_workspaces = plan
111+
.conflicts
112+
.iter()
113+
.filter(|c| c.code == "workspace_target_wins")
114+
.count() as u64;
115+
result.workspaces.imported = result.workspaces.imported.saturating_sub(target_workspaces);
116+
result.workspaces.skipped += target_workspaces;
117+
if let Some(domain) = report
118+
.domain_results
119+
.iter()
120+
.find(|r| r.domain == MigrationDomainId::WorkspaceSessions)
121+
{
122+
for diagnostic in &domain.warnings {
123+
match diagnostic.code.as_str() {
124+
"session_source_skipped" | "session_entry_skipped" => result.sessions.skipped += 1,
125+
"workspace_item_skipped" => result.workspaces.skipped += 1,
126+
"assistant_workspace_not_migrated" => result.assistant_directories.skipped += 1,
127+
_ => {}
128+
}
129+
}
130+
}
131+
Ok(result)
132+
}
133+
57134
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58135
#[serde(rename_all = "snake_case")]
59136
pub(crate) enum SessionImportAction {

0 commit comments

Comments
 (0)