Files
ECC/tests/ci/ito-baskets-skill.test.js
T
Affaan MustafaGitHubCodeRabbitcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
fc1d11839c feat(skills): consolidate Itô market skills into ito-baskets; align ito-training fail-closed contract (#2770)
* feat(skills): consolidate Itô market skills into ito-baskets; align ito-training fail-closed contract

- Replace ito-basket-compare, ito-market-intelligence, ito-data-atlas-agent,
  and ito-trade-planner with one read-only ito-baskets skill (index, compare,
  brief, worksheet modes) preserving every non-advisory, provenance,
  freshness, and recovery contract
- Extend the GET-only client with anonymous basket-index/basket-detail
  commands that validate the ito.public_basket_read.v1 contract and never
  transmit a credential to public routes
- Rewrite ito-training to the same fail-closed availability-check structure
  as ito-inference: pre-spawn rejection, server-verified booking entitlement,
  opaque confirmation-ref, manifest digest binding, idempotent lifecycle
- Update install module, npm files, README/docs catalog counts (287 -> 284),
  and add consolidated contract tests

* test: anchor Itô API origin assertion (CodeQL js/regex/missing-regexp-anchor)

* test: avoid URL-literal substring assertion (CodeQL js/incomplete-url-substring-sanitization)

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-08-12 15:52:26 -04:00

252 lines
14 KiB
JavaScript

/**
* Contract and lifecycle tests for the consolidated Itô baskets data skill.
* No test contacts Itô, opens a browser, or submits an RFQ/order.
*/
"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const { parseArgs, run } = require("../../skills/ito-baskets/scripts/ito-baskets");
const REPO_ROOT = path.join(__dirname, "..", "..");
const SKILL_DIR = path.join(REPO_ROOT, "skills", "ito-baskets");
const SKILL_PATH = path.join(SKILL_DIR, "SKILL.md");
const CLIENT = path.join(SKILL_DIR, "scripts", "ito-baskets.js");
function readJson(relativePath) {
return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8"));
}
function invoke(args, env = {}) {
return spawnSync(process.execPath, [CLIENT, "--json", ...args], {
encoding: "utf8",
env: { PATH: process.env.PATH, ...env },
timeout: 5000,
});
}
const tests = [];
function test(name, fn) { tests.push([name, fn]); }
test("has valid discoverable frontmatter and consolidated trigger phrases", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
assert.match(skill, /^---\nname: ito-baskets\ndescription: [^\n]+\nmetadata:\n {2}origin: ECC\n/);
assert.match(skill, /aliases: ito-basket-compare, ito-market-intelligence, ito-data-atlas-agent, ito-trade-planner/);
const lower = skill.toLowerCase();
for (const phrase of [
"compare this basket", "basket vs", "gap analysis", "stale assumptions", "watchlist",
"event discovery", "venue comparison", "basket theme", "market brief",
"planning worksheet", "basket catalog", "index",
]) {
assert.ok(lower.includes(phrase), `missing trigger phrase: ${phrase}`);
}
});
test("states that it replaces the four former skills and routes their requests", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
assert.match(skill, /replaces the\s+former `ito-basket-compare`, `ito-market-intelligence`, `ito-data-atlas-agent`,\s+and `ito-trade-planner`/);
const modules = readJson("manifests/install-modules.json").modules;
const module = modules.find((candidate) => candidate.id === "prediction-market-skills");
assert.ok(module, "prediction-market-skills module is missing");
assert.ok(module.paths.includes("skills/ito-baskets"), "consolidated skill is not installed by the module");
for (const removed of ["ito-basket-compare", "ito-market-intelligence", "ito-data-atlas-agent", "ito-trade-planner"]) {
assert.ok(!module.paths.includes(`skills/${removed}`), `removed skill still in module: ${removed}`);
assert.ok(!fs.existsSync(path.join(REPO_ROOT, "skills", removed)), `removed skill directory still exists: ${removed}`);
}
assert.strictEqual(module.defaultInstall, false);
const packed = readJson("package.json").files;
assert.ok(packed.includes("skills/ito-baskets/"), "consolidated skill missing from npm files");
for (const removed of ["ito-basket-compare", "ito-market-intelligence", "ito-data-atlas-agent", "ito-trade-planner"]) {
assert.ok(!packed.includes(`skills/${removed}/`), `removed skill still packed: ${removed}`);
}
});
test("preserves the non-advisory, non-executing boundary from all four predecessors", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
assert.match(skill, /never advise the user to buy, sell, hold, hedge, lever, allocate, or size/i);
assert.match(skill, /never place, cancel, route, sign, simulate, or submit/i);
assert.match(skill, /no execution path and no\s+confirmation can give it one/i);
assert.match(skill, /`ecc ito find` submits an\s+authenticated RFQ/);
assert.match(skill, /`ecc ito status` reads RFQ\/procurement status, not\s+basket data/);
assert.match(skill, /UNSUPPORTED_OPERATION/);
assert.match(skill, /prediction-market-risk-review/);
assert.doesNotMatch(skill, /(?:run|invoke|call) `?ecc ito (?:find|status)/i);
assert.match(skill, /never call a trade good, bad, best, optimal,\s+guaranteed, or risk-free/i);
for (const advisory of [/\byou should buy\b/i, /\byou should sell\b/i, /\bbest trade\b/i, /\boptimal size\b/i]) {
assert.doesNotMatch(skill, advisory);
}
});
test("documents anonymous, keyed, and SDK surfaces with scope and credential separation", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
assert.match(skill, /\/api\/baskets\/bootstrap\?stream=1/);
assert.match(skill, /ito\.public_basket_read\.v1/);
assert.match(skill, /\/api\/markets\/hot/);
assert.match(skill, /Keyed developer API\*\* at/);
assert.match(skill, /https:\/\/itomarkets\.com\/api\/v1(?!\d)/, "missing versioned keyed API path");
assert.match(skill, /Authorization: Bearer/);
assert.match(skill, /baskets:read/);
assert.match(skill, /markets:read/);
assert.match(skill, /bkt_\*/);
assert.match(skill, /ito-markets/);
assert.match(skill, /compute device credential[\s\S]*never a\s+substitute|never a\s+substitute[\s\S]*compute device credential/i);
assert.match(skill, /never uses device authorization or `ecc ito login`/i);
assert.match(skill, /x-ito-edge-cache/);
assert.match(skill, /never send credentials to these routes/i);
});
test("documents provenance, deterministic normalization, and recovery contracts", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
for (const field of ["source_type", "source_uri", "retrieved_at", "as_of", "freshness_status", "access_mode"]) {
assert.match(skill, new RegExp(`\\b${field}\\b`), `missing provenance field: ${field}`);
}
for (const code of ["INVALID_INPUT", "AUTH_MISSING", "AUTH_REJECTED", "AUTH_FORBIDDEN", "RATE_LIMITED", "TIMEOUT", "UPSTREAM_ERROR", "INVALID_RESPONSE", "STALE_SOURCE", "UNSUPPORTED_OPERATION"]) {
assert.ok(skill.includes(code), `missing error code: ${code}`);
}
assert.match(skill, /Unicode NFKC/);
assert.match(skill, /24 hours for market\/basket/);
assert.match(skill, /30 days for notes\/research/);
assert.match(skill, /identical output/i);
assert.match(skill, /match.*conflict.*missing.*stale/is);
assert.match(skill, /120 requests\/minute/);
assert.match(skill, /untrusted data/i);
assert.match(skill, /never treat[\s\S]*draft[\s\S]*approval|confirmation during planning is never an order/i);
});
test("keeps every mode disclaimer exact", () => {
const skill = fs.readFileSync(SKILL_PATH, "utf8");
assert.ok(skill.includes("This is market data, not investment or trading advice."));
assert.ok(skill.includes("This comparison is informational and not investment or trading advice."));
assert.ok(skill.includes("This is a planning worksheet, not investment or trading advice. Review venue rules and make any trading decisions yourself."));
});
test("ships agent metadata for the consolidated skill", () => {
const agentMetadata = fs.readFileSync(path.join(SKILL_DIR, "agents", "openai.yaml"), "utf8");
assert.match(agentMetadata, /display_name: "Itô Baskets"/);
assert.match(agentMetadata, /default_prompt: "Use \$ito-baskets /);
});
test("keyed client keeps the GET-only contract and never echoes credentials", async () => {
let result = invoke(["search-markets"]);
assert.strictEqual(result.status, 1);
assert.strictEqual(JSON.parse(result.stderr).error.code, "AUTH_MISSING");
assert.match(JSON.parse(result.stderr).error.message, /anonymous basket-index\/basket-detail/);
result = invoke(["search-markets"], { ITO_API_KEY: "secret", ITO_MARKET_API_URL: "http://example.com/api/v1" });
assert.strictEqual(JSON.parse(result.stderr).error.code, "CONFIG");
assert.ok(!result.stderr.includes("secret"));
const fetchSuccess = async (url, request) => {
assert.strictEqual(request.method, "GET");
assert.strictEqual(request.headers.Authorization, "Bearer test-key");
assert.match(url.toString(), /\/markets\/search\?platform=all&limit=1$/);
return new Response(JSON.stringify({ data: [{ market_id: "m1", title: "Example" }], meta: { updated_at: "2026-08-07T12:00:00Z" } }), { status: 200, headers: { "x-ratelimit-limit": "120", "x-ratelimit-remaining": "119", "x-ratelimit-reset": "1786128733" } });
};
const payload = await run(parseArgs(["node", CLIENT, "search-markets", "--platform", "all", "--limit", "1"]), { ITO_API_KEY: "test-key" }, fetchSuccess);
assert.strictEqual(payload.ok, true);
assert.strictEqual(payload.access_mode, "keyed");
assert.strictEqual(payload.source.provider, "Itô Markets");
assert.strictEqual(payload.freshness.source_updated_at, "2026-08-07T12:00:00Z");
assert.deepStrictEqual(payload.rate_limit, { limit: 120, remaining: 119, reset_epoch: 1786128733 });
assert.deepStrictEqual(payload.data, [{ market_id: "m1", title: "Example" }]);
assert.ok(!JSON.stringify(payload).includes("test-key"));
const fetchPage = async (url) => {
assert.match(url.toString(), /\/baskets\?page=2&per_page=5$/);
return new Response(JSON.stringify({ data: [], meta: { page: 2, per_page: 5 } }), { status: 200 });
};
const pagePayload = await run(parseArgs(["node", CLIENT, "list-baskets", "--page", "2", "--per-page", "5"]), { ITO_API_KEY: "test-key" }, fetchPage);
assert.strictEqual(pagePayload.meta.per_page, 5);
await assert.rejects(
run(parseArgs(["node", CLIENT, "list-baskets"]), { ITO_API_KEY: "revoked" }, async () => new Response("{}", { status: 401 })),
(error) => error.code === "AUTH_REJECTED" && !error.message.includes("revoked")
);
await assert.rejects(
run(parseArgs(["node", CLIENT, "list-baskets"]), { ITO_API_KEY: "key" }, async () => new Response("{}", { status: 429, headers: { "retry-after": "7" } })),
(error) => error.code === "RATE_LIMITED" && error.details.retry_after_seconds === 7
);
await assert.rejects(
run(parseArgs(["node", CLIENT, "--timeout-ms", "100", "list-baskets"]), { ITO_API_KEY: "key" }, async (_url, request) => new Promise((_resolve, reject) => {
request.signal.addEventListener("abort", () => reject(Object.assign(new Error("aborted"), { name: "AbortError" })));
})),
(error) => error.code === "TIMEOUT" && !error.message.includes("key")
);
await assert.rejects(
run(parseArgs(["node", CLIENT, "list-baskets"]), { ITO_API_KEY: "key" }, async () => new Response("<html>bad gateway</html>", { status: 502 })),
(error) => error.code === "INVALID_RESPONSE" && !error.message.includes("bad gateway")
);
});
test("anonymous index commands never send a credential and validate the public contract", async () => {
const indexBody = { contractVersion: "ito.public_basket_read.v1", generated_at: "2026-08-12T00:00:00Z", baskets: [{ basket_id: "b1" }] };
const fetchIndex = async (url, request) => {
assert.strictEqual(request.method, "GET");
assert.strictEqual(request.headers.Authorization, undefined);
assert.strictEqual(url.hostname, "itomarkets.com");
assert.strictEqual(url.pathname, "/api/baskets/bootstrap");
assert.strictEqual(url.search, "?stream=1");
return new Response(JSON.stringify(indexBody), { status: 200, headers: { "cache-control": "public, max-age=30", "x-ito-edge-cache": "HIT" } });
};
// Even with ITO_API_KEY configured, anonymous commands must not transmit it.
const payload = await run(parseArgs(["node", CLIENT, "basket-index"]), { ITO_API_KEY: "must-not-leak" }, fetchIndex);
assert.strictEqual(payload.ok, true);
assert.strictEqual(payload.access_mode, "anonymous");
assert.strictEqual(payload.freshness.source_updated_at, "2026-08-12T00:00:00Z");
assert.strictEqual(payload.cache.edge_cache, "HIT");
assert.ok(!JSON.stringify(payload).includes("must-not-leak"));
await assert.rejects(
run(parseArgs(["node", CLIENT, "basket-index"]), {}, async () => new Response(JSON.stringify({ contractVersion: "ito.public_basket_read.v0", baskets: [] }), { status: 200 })),
(error) => error.code === "INVALID_RESPONSE" && /contract changed or missing/.test(error.message)
);
await assert.rejects(
run(parseArgs(["node", CLIENT, "basket-index"]), {}, async () => new Response(JSON.stringify({ contractVersion: "ito.public_basket_read.v1", generated_at: "2026-08-12T00:00:00Z" }), { status: 200 })),
(error) => error.code === "INVALID_RESPONSE" && /baskets array/.test(error.message)
);
await assert.rejects(
run(parseArgs(["node", CLIENT, "basket-detail", "--basket-id", "b1"]), {}, async () => new Response(JSON.stringify({ contractVersion: "ito.public_basket_read.v1", generated_at: "2026-08-12T00:00:00Z", basket: {}, underlyers: [], charts: {}, metrics: {} }), { status: 200 })),
(error) => error.code === "INVALID_RESPONSE" && /commentary/.test(error.message)
);
const detailBody = { contractVersion: "ito.public_basket_read.v1", generated_at: "2026-08-12T00:00:00Z", basket: { basket_id: "b1" }, underlyers: [], charts: {}, metrics: {}, commentary: {} };
const detail = await run(parseArgs(["node", CLIENT, "basket-detail", "--basket-id", "b1"]), {}, async (url) => {
assert.strictEqual(url.hostname, "itomarkets.com");
assert.strictEqual(url.pathname, "/api/baskets/b1/bootstrap");
return new Response(JSON.stringify(detailBody), { status: 200 });
});
assert.strictEqual(detail.ok, true);
assert.strictEqual(detail.access_mode, "anonymous");
});
test("client rejects unknown commands, mutations, and bad options before any fetch", () => {
for (const args of [["create-basket"], ["delete-basket"], ["order"], ["basket-detail"], ["basket-index", "--page", "1"]]) {
const result = invoke(args, { ITO_API_KEY: "key" });
assert.strictEqual(result.status, 2, `expected USAGE exit 2 for: ${args.join(" ")}`);
assert.strictEqual(JSON.parse(result.stderr).error.code, "USAGE");
}
fs.accessSync(CLIENT, fs.constants.R_OK);
});
(async () => {
let passed = 0;
let failed = 0;
for (const [name, fn] of tests) {
try {
await fn();
console.log(` ✓ ${name}`);
passed += 1;
} catch (error) {
console.log(` ✗ ${name}`);
console.error(` ${error.message}`);
failed += 1;
}
}
console.log(`${passed} passed, ${failed} failed`);
if (failed > 0) process.exitCode = 1;
else console.log("PASS ito-baskets skill contract");
})();