Ctrl K
ComunidadGuías…

test/smart-agent-contracts.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"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;
});