mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
* feat(ai): add host coding agent sessions with configured providers * Wait for agent initialization and shutdown before resuming sessions * Keep Pi transcripts outside its configuration migration directory * Cover Pi session directory separation * Update core permission catalog expectations for remote agents * Install remote agent runtimes from verified official sources * Wait for the identity panel credentials request in its test * Add scoped SSH forwarding setup for remote agents * Verify forwarding setup through a fresh SSH connection * Bound the SSH forwarding verification timeout * Return the forwarding verification callback result * fix(ai): support agent send and interrupt keyboard shortcuts * feat(ai): add agent queues attachments worktrees and session recovery * fix(ai): handle deleted workspace paths and duplicate status events * fix(ai): accept file references in root workspaces * fix(ai): refresh paused queues after agent errors * fix(ai): store agent sessions in plugin tables and lock down OpenCode Agent sessions lived in a single ctx.kv value holding up to 2000 events, which passed the 256 KB kv limit within minutes of a normal run. Every save after that failed, so history, nativeId, queue and archive changes were lost and every event wrote a kv audit row. Sessions, events and queued prompts now have their own tables. A running agent appends events in batches at most once a second with a lazy save; session and queue changes save immediately. Prompts, drafts and stored event text are capped in UTF-8 bytes to fit MySQL TEXT. kv:own is no longer requested. The OpenCode server now requires a per-session password, so other local accounts on the host cannot drive the agent through its loopback port. * test(upgrade): include agent session tables in the reviewed upgrade SQL
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
|
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
|
|
import { useAgentStream } from "../../src/frontend/agents/useAgentStream";
|
|
const api = vi.hoisted(() => ({ get: vi.fn(), fetch: vi.fn() }));
|
|
vi.mock("../../src/frontend/app-ref", () => ({
|
|
aiApp: () => ({ api, fetch: api.fetch }),
|
|
}));
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.resetAllMocks();
|
|
});
|
|
it("restores a snapshot after an EOF and resumes after its last sequence", async () => {
|
|
api.get
|
|
.mockResolvedValueOnce({
|
|
data: {
|
|
id: "x",
|
|
status: "running",
|
|
events: [{ seq: 1, kind: "user", text: "hi" }],
|
|
},
|
|
})
|
|
.mockResolvedValue({
|
|
data: {
|
|
id: "x",
|
|
status: "running",
|
|
events: [
|
|
{ seq: 1, kind: "user", text: "hi" },
|
|
{ seq: 2, kind: "text", text: "recovered" },
|
|
],
|
|
},
|
|
});
|
|
api.fetch.mockResolvedValueOnce(new Response(""));
|
|
let stream!: ReadableStreamDefaultController<Uint8Array>;
|
|
api.fetch.mockImplementation(
|
|
async () =>
|
|
new Response(
|
|
new ReadableStream({
|
|
start(c) {
|
|
stream = c;
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
const snapshot = vi.fn(),
|
|
event = vi.fn();
|
|
const { result, unmount } = renderHook(() =>
|
|
useAgentStream("x", 0, snapshot, event),
|
|
);
|
|
await waitFor(() => expect(result.current).toBe("reconnecting"));
|
|
await waitFor(() => expect(api.fetch).toHaveBeenCalledTimes(2), {
|
|
timeout: 2500,
|
|
});
|
|
expect(api.fetch.mock.calls[1][0]).toBe("agents/x/events?after=2");
|
|
await act(async () => {
|
|
stream.enqueue(
|
|
new TextEncoder().encode(
|
|
'data: {"seq":2,"kind":"text","text":"duplicate"}\n\ndata: {"seq":3,"kind":"text","text":"new"}\n\n',
|
|
),
|
|
);
|
|
});
|
|
expect(event).toHaveBeenCalledTimes(1);
|
|
expect(event).toHaveBeenCalledWith({ seq: 3, kind: "text", text: "new" });
|
|
expect(snapshot.mock.lastCall?.[0].events[1].text).toBe("recovered");
|
|
unmount();
|
|
stream.close();
|
|
});
|
|
it("does not reconnect an inaccessible session", async () => {
|
|
api.get.mockRejectedValue({ response: { status: 403 } });
|
|
const { result } = renderHook(() => useAgentStream("x", 0, vi.fn(), vi.fn()));
|
|
await waitFor(() => expect(result.current).toBe("unavailable"));
|
|
expect(api.fetch).not.toHaveBeenCalled();
|
|
});
|