Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe5b63f3b | ||
|
|
c4145189f9 | ||
|
|
41c95d1b73 | ||
|
|
2671f63d28 | ||
|
|
0986814f38 | ||
|
|
7d93b4ab8b | ||
|
|
6e81920492 | ||
|
|
5b8c0c26ad | ||
|
|
787536a0b4 |
@@ -123,6 +123,9 @@ ignore = [
|
||||
"F821", # undefined name (forward references in type hints)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-third-party = ["alembic"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
@@ -288,8 +288,9 @@ async def test_full_api_workflow(api_client, test_bank_id):
|
||||
# 10. Clean Up
|
||||
# ================================================================
|
||||
|
||||
# Note: No delete bank endpoint in API, so test data remains in DB
|
||||
# Using timestamped bank IDs prevents conflicts between test runs
|
||||
# Clean up the test bank (delete bank endpoint is tested separately)
|
||||
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -488,6 +489,87 @@ async def test_document_deletion_with_slashes_in_id(api_client):
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_bank(api_client):
|
||||
"""Test delete bank endpoint.
|
||||
|
||||
Workflow:
|
||||
1. Create a bank by storing memories
|
||||
2. Verify bank exists with data
|
||||
3. Delete the bank
|
||||
4. Verify bank and all data is deleted
|
||||
"""
|
||||
test_bank_id = f"delete_bank_test_{datetime.now().timestamp()}"
|
||||
|
||||
# 1. Create bank by storing memories with a document
|
||||
response = await api_client.post(
|
||||
f"/v1/default/banks/{test_bank_id}/memories",
|
||||
json={
|
||||
"items": [
|
||||
{
|
||||
"content": "Alice is a software engineer at TechCorp.",
|
||||
"context": "team info",
|
||||
"document_id": "team-doc-1",
|
||||
},
|
||||
{
|
||||
"content": "Bob is the CTO and leads the engineering team.",
|
||||
"context": "team info",
|
||||
"document_id": "team-doc-1",
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
|
||||
# 2. Verify bank exists with data
|
||||
# Check profile
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check stats show data exists
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
|
||||
assert response.status_code == 200
|
||||
stats = response.json()
|
||||
assert stats["total_nodes"] > 0
|
||||
|
||||
# Check documents exist
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()["items"]) > 0
|
||||
|
||||
# Check bank is in list
|
||||
response = await api_client.get("/v1/default/banks")
|
||||
assert response.status_code == 200
|
||||
bank_ids = [b["bank_id"] for b in response.json()["banks"]]
|
||||
assert test_bank_id in bank_ids
|
||||
|
||||
# 3. Delete the bank
|
||||
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
assert response.status_code == 200
|
||||
delete_result = response.json()
|
||||
assert delete_result["success"] is True
|
||||
assert delete_result["deleted_count"] > 0
|
||||
assert "deleted successfully" in delete_result["message"]
|
||||
|
||||
# 4. Verify bank and all data is deleted
|
||||
# Bank should not be in list
|
||||
response = await api_client.get("/v1/default/banks")
|
||||
assert response.status_code == 200
|
||||
bank_ids = [b["bank_id"] for b in response.json()["banks"]]
|
||||
assert test_bank_id not in bank_ids
|
||||
|
||||
# Stats should show zero data (profile auto-creates empty bank)
|
||||
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/stats")
|
||||
assert response.status_code == 200
|
||||
stats = response.json()
|
||||
assert stats["total_nodes"] == 0
|
||||
assert stats["total_documents"] == 0
|
||||
|
||||
# Clean up the auto-created empty bank
|
||||
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_retain(api_client):
|
||||
"""Test asynchronous retain functionality.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ bankId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bankId } = await params;
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await sdk.deleteBank({
|
||||
client: lowLevelClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error("API error deleting bank:", response.error);
|
||||
return NextResponse.json({ error: "Failed to delete bank" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(response.data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error deleting bank:", error);
|
||||
return NextResponse.json({ error: "Failed to delete bank" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/lib/api";
|
||||
import { useBank } from "@/lib/bank-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -14,6 +15,16 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
RefreshCw,
|
||||
Save,
|
||||
@@ -26,6 +37,7 @@ import {
|
||||
Link2,
|
||||
FolderOpen,
|
||||
Activity,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DispositionTraits {
|
||||
@@ -158,7 +170,8 @@ function DispositionEditor({
|
||||
}
|
||||
|
||||
export function BankProfileView() {
|
||||
const { currentBank } = useBank();
|
||||
const router = useRouter();
|
||||
const { currentBank, setCurrentBank, loadBanks } = useBank();
|
||||
const [profile, setProfile] = useState<BankProfile | null>(null);
|
||||
const [stats, setStats] = useState<BankStats | null>(null);
|
||||
const [operations, setOperations] = useState<Operation[]>([]);
|
||||
@@ -166,6 +179,10 @@ export function BankProfileView() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
// Delete state
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// Edit state
|
||||
const [editBackground, setEditBackground] = useState("");
|
||||
const [editDisposition, setEditDisposition] = useState<DispositionTraits>({
|
||||
@@ -226,6 +243,24 @@ export function BankProfileView() {
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleDeleteBank = async () => {
|
||||
if (!currentBank) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await client.deleteBank(currentBank);
|
||||
setShowDeleteDialog(false);
|
||||
setCurrentBank(null);
|
||||
await loadBanks();
|
||||
router.push("/");
|
||||
} catch (error) {
|
||||
console.error("Error deleting bank:", error);
|
||||
alert("Error deleting bank: " + (error as Error).message);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentBank) {
|
||||
loadData();
|
||||
@@ -296,6 +331,10 @@ export function BankProfileView() {
|
||||
<Button onClick={() => setEditMode(true)} size="sm">
|
||||
Edit Profile
|
||||
</Button>
|
||||
<Button onClick={() => setShowDeleteDialog(true)} variant="destructive" size="sm">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Bank
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -547,6 +586,53 @@ export function BankProfileView() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Memory Bank</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Are you sure you want to delete the memory bank{" "}
|
||||
<span className="font-semibold text-foreground">{currentBank}</span>?
|
||||
</p>
|
||||
<p className="text-red-600 dark:text-red-400 font-medium">
|
||||
This action cannot be undone. All memories, entities, documents, and the bank
|
||||
profile will be permanently deleted.
|
||||
</p>
|
||||
{stats && (
|
||||
<p>
|
||||
This will delete {stats.total_nodes} memories, {stats.total_documents}{" "}
|
||||
documents, and {stats.total_links} links.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteBank}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Bank
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,19 @@ export class ControlPlaneClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entire memory bank and all its data
|
||||
*/
|
||||
async deleteBank(bankId: string) {
|
||||
return this.fetchApi<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
deleted_count: number;
|
||||
}>(`/api/banks/${bankId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chunk
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user