tandem-memory 0.6.5

Memory storage and embedding utilities for Tandem
#[cfg(test)]
mod tenant_scope_tests {
    use super::*;
    use crate::types::DEFAULT_EMBEDDING_DIMENSION;
    use tempfile::TempDir;

    async fn setup_test_manager() -> (MemoryManager, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let db_path = temp_dir.path().join("test_memory.db");
        let manager = MemoryManager::new(&db_path).await.unwrap();
        (manager, temp_dir)
    }

    async fn setup_deterministic_test_manager() -> (MemoryManager, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let db_path = temp_dir.path().join("test_memory.db");
        let manager = MemoryManager::new_with_embedding_service(
            &db_path,
            crate::embeddings::EmbeddingService::deterministic_for_tests(
                DEFAULT_EMBEDDING_DIMENSION,
            ),
        )
        .await
        .unwrap();
        (manager, temp_dir)
    }

    fn tenant_scope(org_id: &str, workspace_id: &str) -> MemoryTenantScope {
        MemoryTenantScope {
            org_id: org_id.to_string(),
            workspace_id: workspace_id.to_string(),
            deployment_id: Some("deployment-1".to_string()),
        }
    }

    fn strict_context_for_resource(
        tenant_scope: &MemoryTenantScope,
        resource: tandem_enterprise_contract::ResourceRef,
        data_class: tandem_enterprise_contract::DataClass,
    ) -> tandem_enterprise_contract::StrictTenantContext {
        let tenant_context = tandem_enterprise_contract::TenantContext::explicit_user_workspace(
            tenant_scope.org_id.clone(),
            tenant_scope.workspace_id.clone(),
            tenant_scope.deployment_id.clone(),
            "user-a",
        );
        let principal = tandem_enterprise_contract::PrincipalRef::human_user("user-a");
        let request_principal =
            tandem_enterprise_contract::RequestPrincipal::authenticated_user("user-a", "test");
        let grant = tandem_enterprise_contract::ScopedGrant::new(
            "grant-read",
            principal.clone(),
            resource.clone(),
            tandem_enterprise_contract::GrantSource::Direct,
        )
        .with_permissions(vec![tandem_enterprise_contract::AccessPermission::Read])
        .with_data_classes(vec![data_class]);
        tandem_enterprise_contract::StrictTenantContext::new(
            tenant_context,
            principal,
            tandem_enterprise_contract::AuthorityChain::from_request(request_principal),
            tandem_enterprise_contract::ResourceScope::root(resource),
            tandem_enterprise_contract::AssertionMetadata::new(
                "test",
                "tandem-runtime",
                1,
                u64::MAX,
                "assertion-test",
            ),
        )
        .with_grants(vec![grant])
        .with_data_boundary(tandem_enterprise_contract::DataBoundary::allow(vec![
            data_class,
        ]))
    }

    #[tokio::test]
    async fn promoted_knowledge_item_remains_bound_to_owning_tenant() {
        let (manager, _temp) = setup_test_manager().await;
        let tenant_a = tenant_scope("org-a", "workspace-a");
        let tenant_b = tenant_scope("org-b", "workspace-b");
        let now = chrono::Utc::now().timestamp_millis() as u64;

        let space = KnowledgeSpaceRecord {
            id: "tenant-b-promoted-space".to_string(),
            scope: KnowledgeScope::Project,
            project_id: Some("shared-project".to_string()),
            namespace: Some("support/runbooks".to_string()),
            title: Some("Tenant B runbooks".to_string()),
            description: None,
            trust_level: KnowledgeTrustLevel::Promoted,
            metadata: None,
            created_at_ms: now,
            updated_at_ms: now,
        };
        manager
            .upsert_knowledge_space_for_tenant(&space, &tenant_b)
            .await
            .unwrap();

        let item = KnowledgeItemRecord {
            id: "tenant-b-promoted-item".to_string(),
            space_id: space.id.clone(),
            coverage_key: "shared-project::support/runbooks::billing::refunds".to_string(),
            dedupe_key: "tenant-b-promoted-dedupe".to_string(),
            item_type: "runbook".to_string(),
            title: "Tenant B refund runbook".to_string(),
            summary: Some("Tenant B internal refund steps.".to_string()),
            payload: serde_json::json!({"tenant": "b", "action": "refund"}),
            trust_level: KnowledgeTrustLevel::Working,
            status: crate::types::KnowledgeItemStatus::Working,
            run_id: Some("tenant-b-run".to_string()),
            artifact_refs: vec!["artifact://tenant-b/refunds".to_string()],
            source_memory_ids: vec!["memory://tenant-b/refunds".to_string()],
            freshness_expires_at_ms: None,
            metadata: None,
            created_at_ms: now,
            updated_at_ms: now,
        };
        manager
            .upsert_knowledge_item_for_tenant(&item, &tenant_b)
            .await
            .unwrap();

        let promote = KnowledgePromotionRequest {
            item_id: item.id.clone(),
            target_status: crate::types::KnowledgeItemStatus::Promoted,
            promoted_at_ms: now + 10,
            freshness_expires_at_ms: Some(now + 86_400_000),
            reviewer_id: None,
            approval_id: None,
            reason: Some("ct-03 tenant scope regression".to_string()),
        };
        assert!(manager
            .promote_knowledge_item_for_tenant(&promote, &tenant_a)
            .await
            .unwrap()
            .is_none());

        let promoted = manager
            .promote_knowledge_item_for_tenant(&promote, &tenant_b)
            .await
            .unwrap()
            .expect("tenant-b promotion");
        assert_eq!(
            promoted.item.status,
            crate::types::KnowledgeItemStatus::Promoted
        );
        assert_eq!(
            promoted.coverage.latest_item_id.as_deref(),
            Some(item.id.as_str())
        );

        assert!(manager
            .get_knowledge_item_for_tenant(&item.id, &tenant_a)
            .await
            .unwrap()
            .is_none());
        assert!(manager
            .list_knowledge_items_for_tenant(&space.id, Some(&item.coverage_key), &tenant_a)
            .await
            .unwrap()
            .is_empty());
        assert!(manager
            .get_knowledge_coverage_for_tenant(&item.coverage_key, &space.id, &tenant_a)
            .await
            .unwrap()
            .is_none());
        assert!(manager
            .get_knowledge_item_for_tenant(&item.id, &tenant_b)
            .await
            .unwrap()
            .is_some());
    }

    #[tokio::test]
    async fn retrieve_context_enforces_knowledge_scope_phase_before_prompt_assembly() {
        let (manager, _temp) = setup_deterministic_test_manager().await;
        let tenant_scope = tenant_scope("acme", "hq");
        let resource = tandem_enterprise_contract::ResourceRef::new(
            "acme",
            "hq",
            tandem_enterprise_contract::ResourceKind::KnowledgeSpace,
            "knowledge-space-ops",
        )
        .with_project_id("project-ops");
        let policy = crate::KnowledgeScopePolicy {
            registry_id: "registry-ops".to_string(),
            resource_ref: resource.clone(),
            data_class: tandem_enterprise_contract::DataClass::Confidential,
            collection_id: Some("collection-ops".to_string()),
            source_binding_id: Some("binding-ops".to_string()),
            source_object_id: Some("source-ops".to_string()),
            owner_org_unit_id: Some("ou-ops".to_string()),
            risk_tier: Some("confidential".to_string()),
            allowed_workflow_phases: vec!["draft".to_string()],
            allowed_write_tiers: vec![crate::GovernedMemoryTier::Session],
            allowed_promotion_tiers: vec![crate::GovernedMemoryTier::Project],
            retention_expires_at_ms: Some(u64::MAX),
            required_trust_label: Some(crate::MemoryTrustLabel::HumanApproved),
            promotion_requires_approval: true,
        };
        let embedding = crate::embeddings::EmbeddingService::deterministic_for_tests(
            DEFAULT_EMBEDDING_DIMENSION,
        )
        .embed("scoped operational memory")
        .await
        .unwrap();
        let scoped_chunk = MemoryChunk {
            id: "scoped-ops-memory".to_string(),
            content: "scoped operational memory for draft phase only".to_string(),
            tier: MemoryTier::Session,
            session_id: Some("session-ops".to_string()),
            project_id: Some("project-ops".to_string()),
            source: "workflow_memory".to_string(),
            source_path: None,
            source_mtime: None,
            source_size: None,
            source_hash: None,
            tenant_scope: tenant_scope.clone(),
            created_at: chrono::Utc::now(),
            token_count: 7,
            metadata: Some(policy.metadata_value()),
        };
        manager
            .db()
            .store_chunk(&scoped_chunk, &embedding)
            .await
            .unwrap();

        let (unfiltered_context, _) = manager
            .retrieve_context_with_meta_for_tenant(
                "scoped operational memory",
                Some("project-ops"),
                Some("session-ops"),
                &tenant_scope,
                None,
            )
            .await
            .expect("unfiltered context retrieval");
        assert!(!unfiltered_context
            .format_for_injection()
            .contains("scoped operational memory"));

        let wrong_phase_filter = crate::types::MemoryAccessFilter::strict(
            strict_context_for_resource(
                &tenant_scope,
                resource.clone(),
                tandem_enterprise_contract::DataClass::Confidential,
            ),
            chrono::Utc::now().timestamp_millis() as u64,
        )
        .with_workflow_phase("review");
        let (wrong_phase_context, _) = manager
            .retrieve_context_with_meta_for_tenant_with_access_filter(
                "scoped operational memory",
                Some("project-ops"),
                Some("session-ops"),
                &tenant_scope,
                None,
                Some(&wrong_phase_filter),
            )
            .await
            .expect("wrong phase context retrieval");
        assert!(!wrong_phase_context
            .format_for_injection()
            .contains("scoped operational memory"));

        let draft_filter = crate::types::MemoryAccessFilter::strict(
            strict_context_for_resource(
                &tenant_scope,
                resource,
                tandem_enterprise_contract::DataClass::Confidential,
            ),
            chrono::Utc::now().timestamp_millis() as u64,
        )
        .with_workflow_phase("draft");
        let (draft_context, _) = manager
            .retrieve_context_with_meta_for_tenant_with_access_filter(
                "scoped operational memory",
                Some("project-ops"),
                Some("session-ops"),
                &tenant_scope,
                None,
                Some(&draft_filter),
            )
            .await
            .expect("draft context retrieval");
        assert!(draft_context
            .format_for_injection()
            .contains("scoped operational memory"));
    }
}