Implement GetDel method for space management in JS API

- Added a new GetDel method to the space object, allowing retrieval and immediate deletion of a key's value, enhancing one-time use data handling.
- Updated the JS API to include tests for GetDel functionality, ensuring correct behavior for existing and non-existent keys, as well as complex data structures.
- Removed debug print statements from the Assistant's initializeCapabilities method for cleaner code.
This commit is contained in:
Max 2025-12-06 18:36:06 +08:00
parent 666c1e8d74
commit a683452d22
3 changed files with 279 additions and 5 deletions

View file

@ -8,7 +8,6 @@ import (
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/kun/utils"
"github.com/yaoapp/yao/agent/assistant/handlers"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
@ -472,10 +471,6 @@ func (ast *Assistant) initializeCapabilities(ctx *context.Context, opts *context
return err
}
fmt.Println("--- initializeCapabilities debug ---")
utils.Dump(capabilities)
fmt.Println("--- end initializeCapabilities debug ---")
// Set capabilities in context for output adapters to use
if capabilities != nil {
ctx.Capabilities = capabilities

View file

@ -694,6 +694,40 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value {
delFuncVal := delFunc.GetFunction(v8ctx)
obj.Set("Delete", delFuncVal.Value)
// GetDel method: space.GetDel(key) - Get value and delete immediately
// Convenient for one-time use data (e.g., file metadata passed between agents)
getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
if ctx.Space == nil {
return v8go.Null(iso)
}
if len(info.Args()) < 1 {
return bridge.JsException(info.Context(), "GetDel requires a key argument")
}
key := info.Args()[0].String()
// Get value first
value, err := ctx.Space.Get(key)
if err != nil {
return v8go.Null(iso)
}
// Delete immediately after getting
// Ignore delete errors (key might not exist)
ctx.Space.Delete(key)
// Convert to JavaScript value
jsValue, err := bridge.JsValue(info.Context(), value)
if err != nil {
return v8go.Null(iso)
}
return jsValue
})
getDelFuncVal := getDelFunc.GetFunction(v8ctx)
obj.Set("GetDel", getDelFuncVal.Value)
return spaceObj
}

View file

@ -570,3 +570,248 @@ func TestSpaceErrorHandling(t *testing.T) {
assert.Equal(t, true, result["success"], "Should catch Delete error")
}
// TestSpaceGetDel tests ctx.space.GetDel method
func TestSpaceGetDel(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ctx := &context.Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Locale: "en",
Context: stdContext.Background(),
Space: plan.NewMemorySharedSpace(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Set a one-time use value
ctx.space.Set("one_time_token", "secret_token_12345");
// Verify it exists before GetDel
const before = ctx.space.Get("one_time_token");
if (before !== "secret_token_12345") throw new Error("Value not set");
// Use GetDel - should get value and delete automatically
const value = ctx.space.GetDel("one_time_token");
// Verify value was retrieved
if (value !== "secret_token_12345") throw new Error("GetDel returned wrong value");
// Verify key was deleted
const after = ctx.space.Get("one_time_token");
if (after !== null && after !== undefined) throw new Error("Key should be deleted after GetDel");
return {
success: true,
value: value,
is_deleted: after === null || after === undefined
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, true, result["success"], "GetDel should succeed")
assert.Equal(t, "secret_token_12345", result["value"], "GetDel should return correct value")
assert.Equal(t, true, result["is_deleted"], "Key should be deleted after GetDel")
}
// TestSpaceGetDelNonExistentKey tests GetDel on non-existent key
func TestSpaceGetDelNonExistentKey(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ctx := &context.Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Locale: "en",
Context: stdContext.Background(),
Space: plan.NewMemorySharedSpace(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// GetDel on non-existent key should return null/undefined
const value = ctx.space.GetDel("non_existent_key");
return {
success: true,
value: value,
is_null: value === null || value === undefined
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
assert.Equal(t, true, result["success"], "GetDel on non-existent key should not throw")
assert.Equal(t, true, result["is_null"], "GetDel on non-existent key should return null")
}
// TestSpaceGetDelComplexData tests GetDel with complex data structures
func TestSpaceGetDelComplexData(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ctx := &context.Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Locale: "en",
Context: stdContext.Background(),
Space: plan.NewMemorySharedSpace(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Set complex file info data (like voucher assistant use case)
const filesInfo = [
{
file_id: "file123",
filename: "invoice.pdf",
content_type: "application/pdf",
file_type: "pdf",
source: "uploader",
uploader_name: "__yao.attachment"
},
{
file_id: "file456",
filename: "receipt.png",
content_type: "image/png",
file_type: "image",
source: "uploader",
uploader_name: "__yao.attachment"
}
];
ctx.space.Set("workers.voucher:files_info", filesInfo);
// Use GetDel to retrieve and clean up
const retrieved = ctx.space.GetDel("workers.voucher:files_info");
// Verify data integrity
if (!Array.isArray(retrieved)) throw new Error("Should be array");
if (retrieved.length !== 2) throw new Error("Length mismatch");
if (retrieved[0].file_id !== "file123") throw new Error("First file_id mismatch");
if (retrieved[1].filename !== "receipt.png") throw new Error("Second filename mismatch");
// Verify it's deleted
const after = ctx.space.Get("workers.voucher:files_info");
if (after !== null && after !== undefined) throw new Error("Should be deleted");
return {
success: true,
files_count: retrieved.length,
first_file_id: retrieved[0].file_id,
is_deleted: after === null || after === undefined
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, true, result["success"], "GetDel with complex data should succeed")
assert.Equal(t, float64(2), result["files_count"], "Should have 2 files")
assert.Equal(t, "file123", result["first_file_id"], "File ID should match")
assert.Equal(t, true, result["is_deleted"], "Should be deleted after GetDel")
}
// TestSpaceGetDelMultipleCalls tests that GetDel only works once
func TestSpaceGetDelMultipleCalls(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ctx := &context.Context{
ChatID: "test-chat-id",
AssistantID: "test-assistant-id",
Locale: "en",
Context: stdContext.Background(),
Space: plan.NewMemorySharedSpace(),
}
res, err := v8.Call(v8.CallOptions{}, `
function test(ctx) {
try {
// Set a value
ctx.space.Set("single_use", "use_me_once");
// First GetDel should work
const first = ctx.space.GetDel("single_use");
if (first !== "use_me_once") throw new Error("First GetDel failed");
// Second GetDel should return null (already deleted)
const second = ctx.space.GetDel("single_use");
if (second !== null && second !== undefined) throw new Error("Second GetDel should return null");
// Third GetDel should also return null
const third = ctx.space.GetDel("single_use");
if (third !== null && third !== undefined) throw new Error("Third GetDel should return null");
return {
success: true,
first: first,
second_is_null: second === null || second === undefined,
third_is_null: third === null || third === undefined
};
} catch (error) {
return { success: false, error: error.message };
}
}`, ctx)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
result, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("Expected map result, got %T", res)
}
if !result["success"].(bool) {
t.Fatalf("Test failed: %v", result["error"])
}
assert.Equal(t, true, result["success"], "Multiple GetDel calls should work correctly")
assert.Equal(t, "use_me_once", result["first"], "First GetDel should return value")
assert.Equal(t, true, result["second_is_null"], "Second GetDel should return null")
assert.Equal(t, true, result["third_is_null"], "Third GetDel should return null")
}