Skip to content

Commit 47b8a3d

Browse files
committed
dashboard: fix Memories project picker showing raw 'git:HASH' identities
The v0.21.5 plugin-side fix to issue #87 made the plugin stamp resolved project identities ('git:<hash>' / 'dir:<sha256>') directly into memories.project_path. The dashboard's project picker, however, was still resolving those values as if they were filesystem paths: enumerate_memory_projects collected memory project_path values into a HashSet<String> and passed it as a paths_filter into enumerate_projects_filtered, which compared the values against OpenCode/Pi 'worktree'/'cwd' paths via allowed_paths.contains(...). Identity-shaped values never matched any real path, so the OpenCode/Pi DB enrichment step was skipped and the fallback path seeded ProjectRow with primary_path = 'git:HASH', then display_name = basename('git:HASH') = 'git:HASH'. Result: the picker dropdown showed entries like 'git:0e973b7d…', 'dir:07a38392e402'. Fix: normalize memory project_path values to identities (handling both already-identity-shaped values and legacy raw filesystem paths that still need resolve_project_identity()), then filter the FULL enumerated project list by identity match — using the OpenCode/Pi DBs as the source of truth for display_name and primary_path. Same shape as the plugin-side fix. Tests: 3 new (identity memories never leak as display_name, archived-only memories don't surface, empty DB returns empty).
1 parent 5c00b50 commit 47b8a3d

1 file changed

Lines changed: 94 additions & 2 deletions

File tree

  • packages/dashboard/src-tauri/src

packages/dashboard/src-tauri/src/db.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,11 +1371,42 @@ pub fn enumerate_memory_projects(conn: &Connection) -> Result<Vec<ProjectRow>, r
13711371
WHERE status = 'active'
13721372
ORDER BY project_path",
13731373
)?;
1374-
let project_paths: HashSet<String> = stmt
1374+
let memory_project_values: HashSet<String> = stmt
13751375
.query_map([], |row| row.get(0))?
13761376
.collect::<Result<HashSet<_>, _>>()?;
13771377

1378-
Ok(enumerate_projects_filtered(Some(&project_paths)))
1378+
// Each memory `project_path` is canonically one of:
1379+
// - a resolved project identity (`git:<hash>` or `dir:<sha256>`) for
1380+
// memories written by the post-#87 plugin where storage stamps the
1381+
// identity directly, or
1382+
// - a raw filesystem path for legacy memories written before identity
1383+
// normalization landed on the plugin side.
1384+
//
1385+
// Normalize every value to its identity. For identity-shaped strings the
1386+
// value is itself the identity. For real paths we resolve through git +
1387+
// SHA256 fallback. This is the SAME normalization the v0.21.5 plugin-side
1388+
// fix to issue #87 performs on the query side — keeping the dashboard
1389+
// aligned with it so the project picker matches the memory pool by
1390+
// identity, not by raw path.
1391+
let memory_identities: HashSet<String> = memory_project_values
1392+
.iter()
1393+
.map(|value| {
1394+
if value.starts_with("git:") || value.starts_with("dir:") {
1395+
value.clone()
1396+
} else {
1397+
resolve_project_identity(value)
1398+
}
1399+
})
1400+
.collect();
1401+
1402+
// Build the full project list from OpenCode + Pi DBs (which gives us the
1403+
// real `display_name` and `primary_path` for each project), then filter
1404+
// down to projects whose identity has at least one memory in the pool.
1405+
let all = enumerate_projects_filtered(None);
1406+
Ok(all
1407+
.into_iter()
1408+
.filter(|row| memory_identities.contains(&row.identity))
1409+
.collect())
13791410
}
13801411

13811412
fn enumerate_projects_filtered(project_paths_filter: Option<&HashSet<String>>) -> Vec<ProjectRow> {
@@ -4189,4 +4220,65 @@ mod memory_project_filter_tests {
41894220
assert_eq!(stats.permanent, 1);
41904221
assert_eq!(stats.archived, 1);
41914222
}
4223+
4224+
/// Regression: `enumerate_memory_projects` returned rows whose
4225+
/// `display_name` was the raw identity (e.g. `git:abc…`, `dir:abc…`)
4226+
/// because it passed memory `project_path` values as a paths filter into
4227+
/// `enumerate_projects_filtered`. When the plugin started stamping
4228+
/// identities directly into memories.project_path (post-issue-#87 plugin
4229+
/// fix), those values never matched any `worktree`/`cwd` path, so the
4230+
/// OpenCode/Pi DB enrichment step was skipped and the fallback path
4231+
/// seeded ProjectRow with `primary_path = "git:HASH"`, then
4232+
/// `display_name = basename("git:HASH") = "git:HASH"`.
4233+
///
4234+
/// Fix: filter the full enumerated project list by identity instead of
4235+
/// filtering by path string. These tests pin both arms:
4236+
/// - identity-shaped memory values map to themselves
4237+
/// - raw filesystem paths get resolved through `resolve_project_identity`
4238+
/// In both cases the returned ProjectRow display_name MUST NOT start
4239+
/// with `git:` or `dir:`. With no OpenCode/Pi DB in the test sandbox the
4240+
/// list is empty rather than poisoned with identity-named rows; that's
4241+
/// the correct safe failure mode.
4242+
#[test]
4243+
fn enumerate_memory_projects_with_identity_memories_does_not_leak_identity_as_name() {
4244+
// Simulate the post-#87 plugin storing the resolved identity directly
4245+
// as project_path. Pre-fix this produced ProjectRow rows with
4246+
// `display_name = "git:abc123…"`. Post-fix the list is empty because
4247+
// no real OpenCode/Pi DB is attached in the test sandbox — that's
4248+
// the correct safe state; the previous behavior was an active bug.
4249+
let conn = make_memory_db();
4250+
insert_memory(&conn, "git:abc1234567890abcdef", "CONSTRAINTS", "active");
4251+
insert_memory(&conn, "git:abc1234567890abcdef", "ARCHITECTURE_DECISIONS", "active");
4252+
insert_memory(&conn, "dir:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", "USER_DIRECTIVES", "active");
4253+
4254+
let rows = enumerate_memory_projects(&conn).expect("enumerate");
4255+
for row in &rows {
4256+
assert!(
4257+
!row.display_name.starts_with("git:") && !row.display_name.starts_with("dir:"),
4258+
"display_name leaked identity: {} (identity={})",
4259+
row.display_name,
4260+
row.identity,
4261+
);
4262+
}
4263+
}
4264+
4265+
#[test]
4266+
fn enumerate_memory_projects_with_only_archived_memories_returns_empty() {
4267+
// `enumerate_memory_projects` filters memories by `status = 'active'`.
4268+
// A project whose only memories are archived should not appear in the
4269+
// picker at all. (This was already the behavior before the fix; we
4270+
// pin it so the new identity-filtering path doesn't accidentally
4271+
// surface archived-only projects.)
4272+
let conn = make_memory_db();
4273+
insert_memory(&conn, "/tmp/archived-only", "X", "archived");
4274+
let rows = enumerate_memory_projects(&conn).expect("enumerate");
4275+
assert!(rows.is_empty(), "archived-only project leaked: {rows:?}");
4276+
}
4277+
4278+
#[test]
4279+
fn enumerate_memory_projects_empty_db_returns_empty() {
4280+
let conn = make_memory_db();
4281+
let rows = enumerate_memory_projects(&conn).expect("enumerate");
4282+
assert!(rows.is_empty(), "empty db should produce empty picker, got {rows:?}");
4283+
}
41924284
}

0 commit comments

Comments
 (0)