test/smart-agent-contracts.test.js
Rama main · réplica de lectura del repositorio original.
"use strict";
const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const AiChat = require("../lib/ai-chat");
class MockRun {
async *stream() {
yield {
type: "tool_call",
name: "edit",
call_id: "edit-1",
args: { path: "src/file.js", old_string: "old", new_string: "new" },
status: "completed",
result: { status: "success", value: { path: "src/file.js", linesAdded: 1, linesRemoved: 1 } },
};
yield { type: "usage", usage: { inputTokens: 12, outputTokens: 5, totalTokens: 17 } };
yield { type: "assistant", text: "updated" };
}
async wait() {
return { status: "finished" };
}
supports(name) {
return name === "cancel";
}
async cancel() {}
}
class MockAgent {
constructor(provider) {
this.provider = provider;
this.agentId = provider + "-agent";
this.closed = false;
}
async send() {
return new MockRun();
}
getHistory() {
return [{ role: "assistant", content: "updated" }];
}
async close() {
this.closed = true;
}
}
class MockProviderFactory {
constructor() {
this.calls = [];
this.agents = [];
}
cursorStatus() {
return { available: true, error: "" };
}
async create(options) {
this.calls.push(options);
const agent = new MockAgent(options.provider);
this.agents.push(agent);
return { agent, prelude: "" };
}
async deleteCursorAgent() {}
}
function grokAuth() {
return {
pending: null,
connected: () => true,
status: () => ({ connected: true, source: "mock", emailMasked: "m\u2026@example.com" }),
getAccessToken: async () => "mock-token",
};
}
async function providerContract(provider) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vrcloud-contract-"));
const workspace = path.join(root, "workspace");
fs.mkdirSync(path.join(workspace, "src"), { recursive: true });
fs.writeFileSync(path.join(workspace, "src", "file.js"), "module.exports = \"old\";\n");
const factory = new MockProviderFactory();
let snapshot = 0;
const checkpoints = {
available: true,
snapshot: async () => (++snapshot === 1 ? "aaaaaaa" : "bbbbbbb"),
diffStat: async () => [{ path: "src/file.js", added: 1, removed: 1 }],
};
const events = [];
const changed = [];
const chat = new AiChat({
workspace,
configFile: path.join(root, "ai-config.json"),
storeFile: path.join(root, "ai-sessions.json"),
provider,
apiKey: "cursor-test-key",
anthropicKey: "anthropic-test-key",
grokAuth: grokAuth(),
providerFactory: factory,
checkpoints,
orchestration: "off",
onFsChange: (paths) => changed.push(paths),
});
try {
const sessionId = provider + "-session";
const status = await chat.send(sessionId, "Update src/file.js", (kind, value) => {
events.push({ kind, value });
}, { orchestrate: false });
assert.strictEqual(status, "finished");
assert.strictEqual(factory.calls.length, 1);
assert.strictEqual(factory.calls[0].provider, provider);
assert.strictEqual(typeof factory.agents[0].send, "function");
assert.strictEqual(typeof factory.agents[0].close, "function");
const session = chat.getSession(sessionId);
const message = session.messages.find((item) => item.role === "ai");
assert.ok(message);
assert.strictEqual(message.text, "updated");
assert.strictEqual(message.before, "aaaaaaa");
assert.strictEqual(message.after, "bbbbbbb");
assert.deepStrictEqual(message.files, ["src/file.js"]);
assert.deepStrictEqual(message.changes, [{ path: "src/file.js", added: 1, removed: 1 }]);
// inputTokens/outputTokens/totalTokens diakumulasi lintas putaran; contextTokens adalah
// okupansi jendela konteks pada putaran terakhir (input + cache + output) dan contextLimit
// jendela model aktif — keduanya dipakai bar konteks di panel AI.
assert.deepStrictEqual(message.usage, {
inputTokens: 12, outputTokens: 5, totalTokens: 17,
contextTokens: 17, contextLimit: 200000,
});
assert.ok(events.some((event) => event.kind === "checkpoint"));
assert.ok(events.some((event) => event.kind === "changes"));
assert.ok(changed.some((paths) => paths.includes("src/file.js")));
const review = chat.setReview(sessionId, "bbbbbbb", {
"src\\file.js": "accepted",
"ignored.js": "pending",
});
assert.deepStrictEqual(review, { "src/file.js": "accepted" });
const reviewed = chat.getSession(sessionId).messages.find((item) => item.role === "ai");
assert.deepStrictEqual(reviewed.review, { "src/file.js": "accepted" });
} finally {
await chat.disposeAll();
fs.rmSync(root, { recursive: true, force: true });
}
}
async function resetCancelsBeforeDeletion() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vrcloud-reset-"));
let chat = null;
let cancelled = false;
const orchestrator = {
activeRuns: new Map(),
shouldRun: () => false,
cancel: async () => true,
cancelSession: async (sessionId) => {
assert.ok(chat.sessions.has(sessionId), "runtime was deleted before orchestration cancellation");
assert.ok(chat.getSession(sessionId), "stored session was deleted before orchestration cancellation");
await new Promise((resolve) => setTimeout(resolve, 10));
cancelled = true;
return true;
},
};
chat = new AiChat({
workspace: root,
storeFile: path.join(root, "ai-sessions.json"),
orchestrator,
providerFactory: new MockProviderFactory(),
});
const sessionId = "reset-active";
chat.createSession(sessionId);
chat.runtime(sessionId);
await chat.reset(sessionId);
assert.strictEqual(cancelled, true);
assert.strictEqual(chat.sessions.has(sessionId), false);
assert.strictEqual(chat.getSession(sessionId), null);
await chat.close();
fs.rmSync(root, { recursive: true, force: true });
}
(async () => {
for (const provider of ["cursor", "anthropic", "grok"]) {
await providerContract(provider);
}
await resetCancelsBeforeDeletion();
console.log("smart-agent-contracts: ok");
})().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});