mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05: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
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { beforeEach, afterEach, expect, it } from "vitest";
|
|
import {
|
|
mkdtempSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
mkdirSync,
|
|
symlinkSync,
|
|
rmSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { spawnSync, execFileSync } from "node:child_process";
|
|
import { REMOTE_WORKSPACE } from "../../../src/backend/agents/workspace.js";
|
|
let home: string, cwd: string;
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), "termix-workspace-"));
|
|
cwd = join(home, "repo");
|
|
mkdirSync(cwd);
|
|
});
|
|
afterEach(() => rmSync(home, { recursive: true, force: true }));
|
|
function run(body: unknown) {
|
|
const r = spawnSync(process.execPath, ["-e", REMOTE_WORKSPACE], {
|
|
env: { ...process.env, HOME: home },
|
|
input: JSON.stringify({ cwd, id: "test", body }),
|
|
encoding: "utf8",
|
|
});
|
|
if (r.status) throw Error(r.stderr);
|
|
return JSON.parse(r.stdout);
|
|
}
|
|
function git(...args: string[]) {
|
|
return execFileSync("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
}
|
|
it("uploads an image without trusting the caller's MIME type and preserves exact bytes", () => {
|
|
const data = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 1, 2]);
|
|
const a = run({
|
|
operation: "upload",
|
|
name: "image.png",
|
|
data: data.toString("base64"),
|
|
});
|
|
expect(a.mime).toBe("image/png");
|
|
expect(readFileSync(a.path)).toEqual(data);
|
|
expect(
|
|
a.path.startsWith(
|
|
join(home, ".local/state/termix-agents/test/attachments"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
it("rejects oversized files, directories and references outside the workspace", () => {
|
|
writeFileSync(join(home, "outside"), "secret");
|
|
symlinkSync(join(home, "outside"), join(cwd, "escape"));
|
|
writeFileSync(join(cwd, "large"), Buffer.alloc(1024 * 1024 + 1));
|
|
for (const path of ["../outside", "escape", "large", "."])
|
|
expect(() => run({ operation: "reference", path })).toThrow();
|
|
});
|
|
it("shows staged, unstaged and untracked changes and creates an isolated branch", () => {
|
|
git("init");
|
|
git("config", "user.name", "Test");
|
|
git("config", "user.email", "test@example.com");
|
|
mkdirSync(join(cwd, "nested"));
|
|
writeFileSync(join(cwd, "nested", "deleted.txt"), "nested base\n");
|
|
writeFileSync(join(cwd, "file.txt"), "base\n");
|
|
git("add", ".");
|
|
git("commit", "-m", "initial");
|
|
writeFileSync(join(cwd, "file.txt"), "staged\n");
|
|
git("add", ".");
|
|
writeFileSync(join(cwd, "file.txt"), "working\n");
|
|
writeFileSync(join(cwd, "new.txt"), "untracked");
|
|
expect(run({ operation: "status" }).files).toContainEqual({
|
|
status: "??",
|
|
path: "new.txt",
|
|
});
|
|
const diff = run({ operation: "diff", path: "file.txt" }).diff;
|
|
expect(diff).toContain("+working");
|
|
expect(diff).toContain("+staged");
|
|
expect(run({ operation: "diff", path: "new.txt" }).diff).toContain(
|
|
"+untracked",
|
|
);
|
|
const wt = run({ operation: "worktree", branch: "termix/test" });
|
|
expect(readFileSync(join(wt.path, "file.txt"), "utf8")).toBe("base\n");
|
|
expect(readFileSync(join(cwd, "file.txt"), "utf8")).toBe("working\n");
|
|
expect(() => run({ operation: "worktree", branch: "termix/test" })).toThrow();
|
|
rmSync(join(cwd, "nested"), { recursive: true });
|
|
expect(run({ operation: "diff", path: "nested/deleted.txt" }).diff).toContain(
|
|
"-nested base",
|
|
);
|
|
rmSync(join(cwd, "file.txt"));
|
|
expect(run({ operation: "diff", path: "file.txt" }).diff).toContain(
|
|
"-staged",
|
|
);
|
|
});
|