rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared";
|
||||
import { DEFAULT_OPENAI_MODEL } from "@nexus/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../ai-client.js", () => ({
|
||||
loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn: () => Promise<unknown>) => fn()),
|
||||
parseAiError: vi.fn((error: unknown) => error instanceof Error ? error.message : String(error)),
|
||||
parseAiError: vi.fn((error: unknown) => (error instanceof Error ? error.message : String(error))),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({
|
||||
@@ -29,12 +29,14 @@ vi.mock("../router/assistant-approvals.js", () => {
|
||||
return {
|
||||
AssistantApprovalStorageUnavailableError,
|
||||
createPendingAssistantApproval: vi.fn(),
|
||||
toApprovalPayload: vi.fn((approval: { id: string; toolName: string; summary: string }, status: string) => ({
|
||||
id: approval.id,
|
||||
toolName: approval.toolName,
|
||||
summary: approval.summary,
|
||||
status,
|
||||
})),
|
||||
toApprovalPayload: vi.fn(
|
||||
(approval: { id: string; toolName: string; summary: string }, status: string) => ({
|
||||
id: approval.id,
|
||||
toolName: approval.toolName,
|
||||
summary: approval.summary,
|
||||
status,
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,14 +65,13 @@ function createClient(...responses: unknown[]) {
|
||||
return {
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn()
|
||||
.mockImplementation(async () => {
|
||||
const next = responses.shift();
|
||||
if (!next) {
|
||||
throw new Error("No mock AI response configured");
|
||||
}
|
||||
return next;
|
||||
}),
|
||||
create: vi.fn().mockImplementation(async () => {
|
||||
const next = responses.shift();
|
||||
if (!next) {
|
||||
throw new Error("No mock AI response configured");
|
||||
}
|
||||
return next;
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -110,7 +111,10 @@ function createLoopInput(overrides: Partial<Parameters<typeof runAssistantToolLo
|
||||
describe("assistant chat loop", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(checkAiOutput).mockImplementation((content: string) => ({ clean: true, redacted: content }));
|
||||
vi.mocked(checkAiOutput).mockImplementation((content: string) => ({
|
||||
clean: true,
|
||||
redacted: content,
|
||||
}));
|
||||
vi.mocked(buildAssistantInsight).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
@@ -121,21 +125,27 @@ describe("assistant chat loop", () => {
|
||||
summary: "create project (name=Apollo)",
|
||||
} as never);
|
||||
|
||||
const result = await runAssistantToolLoop(createLoopInput({
|
||||
client: createClient({
|
||||
choices: [{
|
||||
message: {
|
||||
tool_calls: [{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "create_project",
|
||||
arguments: "{\"name\":\"Apollo\"}",
|
||||
const result = await runAssistantToolLoop(
|
||||
createLoopInput({
|
||||
client: createClient({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "create_project",
|
||||
arguments: '{"name":"Apollo"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
role: "assistant",
|
||||
@@ -147,15 +157,17 @@ describe("assistant chat loop", () => {
|
||||
},
|
||||
});
|
||||
expect(executeTool).not.toHaveBeenCalled();
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
|
||||
entityName: "create_project",
|
||||
summary: "AI tool blocked pending confirmation: create_project",
|
||||
}));
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityName: "create_project",
|
||||
summary: "AI tool blocked pending confirmation: create_project",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("continues after read-only tool calls and returns collected actions and insights", async () => {
|
||||
vi.mocked(executeTool).mockResolvedValue({
|
||||
content: "{\"resources\":1}",
|
||||
content: '{"resources":1}',
|
||||
data: { resources: 1 },
|
||||
action: { type: "navigate", href: "/resources" },
|
||||
} as never);
|
||||
@@ -166,44 +178,54 @@ describe("assistant chat loop", () => {
|
||||
metrics: [{ label: "Resolved holidays", value: "1" }],
|
||||
});
|
||||
|
||||
const result = await runAssistantToolLoop(createLoopInput({
|
||||
client: createClient(
|
||||
{
|
||||
choices: [{
|
||||
message: {
|
||||
tool_calls: [{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: "{\"query\":\"Alice\"}",
|
||||
const result = await runAssistantToolLoop(
|
||||
createLoopInput({
|
||||
client: createClient(
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: '{"query":"Alice"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
choices: [{ message: { content: "Hier ist die passende Resource." } }],
|
||||
},
|
||||
),
|
||||
}));
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
choices: [{ message: { content: "Hier ist die passende Resource." } }],
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
content: "Hier ist die passende Resource.",
|
||||
actions: [{ type: "navigate", href: "/resources" }],
|
||||
insights: [{
|
||||
kind: "holiday_region",
|
||||
title: "Berlin",
|
||||
}],
|
||||
insights: [
|
||||
{
|
||||
kind: "holiday_region",
|
||||
title: "Berlin",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(executeTool).toHaveBeenCalledWith(
|
||||
"search_resources",
|
||||
"{\"query\":\"Alice\"}",
|
||||
'{"query":"Alice"}',
|
||||
expect.objectContaining({ userId: "user_1" }),
|
||||
);
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
|
||||
entityName: "search_resources",
|
||||
summary: "AI executed tool: search_resources",
|
||||
}));
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityName: "search_resources",
|
||||
summary: "AI executed tool: search_resources",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts unsafe AI output before returning it", async () => {
|
||||
@@ -212,61 +234,77 @@ describe("assistant chat loop", () => {
|
||||
redacted: "[redacted]",
|
||||
});
|
||||
|
||||
const result = await runAssistantToolLoop(createLoopInput({
|
||||
client: createClient({
|
||||
choices: [{ message: { content: "API key is sk-secret" } }],
|
||||
const result = await runAssistantToolLoop(
|
||||
createLoopInput({
|
||||
client: createClient({
|
||||
choices: [{ message: { content: "API key is sk-secret" } }],
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
expect(result.content).toBe("[redacted]");
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
{ userId: "user_1" },
|
||||
"AI output contained sensitive content — redacted before delivery",
|
||||
);
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
|
||||
entityName: "AiOutputRedacted",
|
||||
}));
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityName: "AiOutputRedacted",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a stable fallback after too many tool-call iterations", async () => {
|
||||
vi.mocked(executeTool).mockResolvedValue({
|
||||
content: "{\"ok\":true}",
|
||||
content: '{"ok":true}',
|
||||
data: { ok: true },
|
||||
} as never);
|
||||
|
||||
const result = await runAssistantToolLoop(createLoopInput({
|
||||
client: createClient(
|
||||
{
|
||||
choices: [{
|
||||
message: {
|
||||
tool_calls: [{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: "{\"query\":\"A\"}",
|
||||
const result = await runAssistantToolLoop(
|
||||
createLoopInput({
|
||||
client: createClient(
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: '{"query":"A"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
choices: [{
|
||||
message: {
|
||||
tool_calls: [{
|
||||
id: "call_2",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: "{\"query\":\"B\"}",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_2",
|
||||
function: {
|
||||
name: "search_resources",
|
||||
arguments: '{"query":"B"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
),
|
||||
maxToolIterations: 2,
|
||||
}));
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
maxToolIterations: 2,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.content).toBe("I had to stop after too many tool calls. Please try a simpler question.");
|
||||
expect(result.content).toBe(
|
||||
"I had to stop after too many tool calls. Please try a simpler question.",
|
||||
);
|
||||
});
|
||||
|
||||
it("degrades mutation confirmations when approval storage is unavailable", async () => {
|
||||
@@ -274,21 +312,27 @@ describe("assistant chat loop", () => {
|
||||
new AssistantApprovalStorageUnavailableError("missing table"),
|
||||
);
|
||||
|
||||
const result = await runAssistantToolLoop(createLoopInput({
|
||||
client: createClient({
|
||||
choices: [{
|
||||
message: {
|
||||
tool_calls: [{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "create_project",
|
||||
arguments: "{\"name\":\"Apollo\"}",
|
||||
const result = await runAssistantToolLoop(
|
||||
createLoopInput({
|
||||
client: createClient({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
function: {
|
||||
name: "create_project",
|
||||
arguments: '{"name":"Apollo"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
expect(result.content).toContain("Schreibende Assistant-Aktionen sind gerade nicht verfuegbar");
|
||||
expect(executeTool).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user