Files
agents-marketplace/gateway/test/migration.test.ts
david 4ebe7c9683 feat(gateway,supplier,shared): shared demo key with session-backed OpenAI endpoint + tool-calling
MVP demo: one operator-funded API key anyone can plug into an IDE/harness,
running real escrow-per-session marketplace conversations (llm.chat.v1)
behind the standard OpenAI /chat/completions wire shape.

- shared: ChatMessage gains tool role + tool_calls; normalizeChatMessage/
  chatMessagesEquivalent keep supplier transcript and gateway mirror
  hash-identical for receipts (field presence is part of the identity)
- supplier: /v1/chat/message accepts {messages,tools,tool_choice} alongside
  legacy {content}; callOpenAiStream forwards tools, accumulates fragmented
  delta.tool_calls, allows content-less tool turns; done frame carries the
  assistant message verbatim + finish_reason + usage
- gateway: demo flag on api_keys (additive migration), create-demo-key CLI,
  withdraw 403 + hidden deposit address for demo keys, per-IP demo rate
  limit (demo keys skip the shared per-key limit), models filtered to chat
  capability
- gateway executor (demoChat.ts): transcript-prefix affinity maps stateless
  calls onto open sessions, forwards only unseen deltas, transparently
  reopens on supplier idle-close; never falls back to one-shot (its settle
  path assumes <=1 in-flight escrow per wallet)
- mutex scope: key mutex held only for chain work (open/close); off-chain
  turns serialize on per-session locks so replicas serve users concurrently
- sweeper: chat-aware reclaim guard (35min vs 11min) and abandoned-session
  cleanup; terminal state confirmed via on-chain queryUtxo (indexer keeps
  lineage rows at their old state forever)
- deploy: kimi-chat replicas 2+3 compose files; chat idle timeout 2.5min

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:30:17 +02:00

80 lines
2.6 KiB
TypeScript

/**
* migration.test.ts — the demo-column migration on pre-existing databases.
*
* GatewayStore has no migration framework (pure CREATE TABLE IF NOT EXISTS),
* so opening a DB created before the `demo` column must ALTER it in place.
*/
import { describe, it, expect } from "vitest";
import { tmpdir } from "os";
import { join } from "path";
import { mkdirSync } from "fs";
import { randomUUID } from "crypto";
import Database from "better-sqlite3";
import { GatewayStore } from "../src/db/store.js";
const LEGACY_API_KEYS_SQL = `
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
label TEXT,
wallet_pkh TEXT NOT NULL,
deposit_address TEXT NOT NULL,
enc_priv_nonce TEXT NOT NULL,
enc_priv_ct TEXT NOT NULL,
enc_priv_tag TEXT NOT NULL,
master_key_version INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
disabled INTEGER NOT NULL DEFAULT 0
);
`;
function keyRow(id: string) {
return {
id,
key_hash: `hash-${id}`,
key_prefix: "vk_test_xxxx",
label: null,
wallet_pkh: "pkh",
deposit_address: "addr_test1x",
enc_priv_nonce: "n",
enc_priv_ct: "c",
enc_priv_tag: "t",
master_key_version: 1,
created_at: Date.now(),
};
}
describe("GatewayStore demo-column migration", () => {
it("adds the demo column to a legacy DB and defaults existing keys to 0", () => {
const dir = join(tmpdir(), `gw-mig-${randomUUID()}`);
mkdirSync(dir, { recursive: true });
const raw = new Database(join(dir, "gateway.db"));
raw.exec(LEGACY_API_KEYS_SQL);
raw
.prepare(
`INSERT INTO api_keys (id, key_hash, key_prefix, label, wallet_pkh, deposit_address,
enc_priv_nonce, enc_priv_ct, enc_priv_tag, master_key_version, created_at, disabled)
VALUES (@id, @key_hash, @key_prefix, @label, @wallet_pkh, @deposit_address,
@enc_priv_nonce, @enc_priv_ct, @enc_priv_tag, @master_key_version, @created_at, 0)`,
)
.run(keyRow("legacy"));
raw.close();
const store = new GatewayStore(dir);
const legacy = store.getKeyByHash("hash-legacy");
expect(legacy?.demo).toBe(0);
store.insertKey({ ...keyRow("demo-key"), demo: 1 });
expect(store.getKeyByHash("hash-demo-key")?.demo).toBe(1);
});
it("persists the demo flag on fresh databases (default 0)", () => {
const dir = join(tmpdir(), `gw-fresh-${randomUUID()}`);
const store = new GatewayStore(dir);
store.insertKey(keyRow("plain"));
expect(store.getKeyByHash("hash-plain")?.demo).toBe(0);
});
});