Files
ruvnet--RuView/tools/ruview-mcp/tests/manifest.test.ts
T
Claude 9109269329 fix(npm): implement ADR-263/264/265 — harness fail-closed + async MCP, rvagent packaging/transport/naming, npm CI+provenance gate
ADR-263 (@ruvnet/ruview 0.2.0), O1-O9:
- claim-check fails closed on empty input (CLI exit 2, empty_text tool error)
- MCP stdio server dispatches tools/call asynchronously (promise-based spawn);
  ping answers while a 3s fake verify runs — pinned by new e2e test
- optionalDependencies dropped: cold npx installs exactly 1 package
  (MEASURED: was 4 pkgs/620kB/71 files via npm i in a clean prefix)
- bounded rolling output tails replace spawnSync 1MiB maxBuffer
- node_monitor port passed via sys.argv, never spliced into python -c source
- serverInfo.version read from package.json; resources/prompts stubs
- skills single-sourced: prepack sync script generates .claude/skills/ copies
- which() = memoized dep-free PATH scan
- tools underscore-canonical (ruview_claim_check, ...) + dotted aliases
- guardrail precision: word-boundary map/f1/auc/iou, code-span + F1/O2 label
  scrubbing, quantitative-claims-only; packaging reproducer hints
- 30/30 tests (was 17), incl. concurrency e2e + fail-open regression pins

ADR-264 (@ruvnet/rvagent 0.2.0), O1-O9:
- exports fixed: types-first, phantom dist/index.cjs require target removed
- tarball map-free: 127,704B unpacked / 46 files / 0 maps (MEASURED,
  npm pack --dry-run; was 188kB incl. 44 maps referencing unshipped src)
- Streamable HTTP actually wired behind RVAGENT_HTTP_PORT: one transport +
  one MCP server per session (mcp-session-id routing), 1MiB body cap (413),
  port-aware localhost origin gate; dual-transport description now true
- tools renamed underscore-canonical with dotted router-only aliases
- single Zod validation gate; advertised inputSchema generated from the same
  Zod source (zod-to-json-schema)
- train_count: parent log fds closed (was leaking 2/job); job records
  persisted to <jobsDir>/<id>.json (job_status survives restarts); bounded
  log-tail reads
- detectCogBinary probes its candidates instead of dead-coding them
- version from package.json; @types/express dropped; @types/jest -> 29
- README rewritten to match reality (no phantom subcommands/policy layer)
- 99/99 jest tests (incl. new session/body-cap suite + previously-broken
  manifest suite); stdio handshake + HTTP session flow smoke-tested live

ADR-265 D1-D4:
- .github/workflows/npm-packages.yml: 3-package x Node 20/22 gate — tests,
  version-literal grep (D3), pack-content/size gate, tarball-install smoke
  test (catches the ADR-264 F1 class), README claim-check (D4)
- .github/workflows/ruview-npm-release.yml: publish from CI only with
  npm publish --provenance
- @ruv/ruview-cli bin renamed ruview-cli (ruview bin belongs to
  @ruvnet/ruview); version single-sourced
- ci.yml NODE_VERSION 18 -> 20

ADR statuses updated to Accepted/implemented; harness manifest re-pinned;
ADR-263/264/265 + both package READMEs pass claim-check.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
2026-07-02 13:03:20 +00:00

102 lines
3.7 KiB
TypeScript

/**
* ADR-124 §2 manifest validation test.
*
* Guards that package.json satisfies every structural decision from ADR-124 §2:
* 1. Package name is @ruvnet/rvagent
* 2. Version is >= 0.1.0
* 3. engines.node is >= 20
* 4. bin includes the "rvagent" key (npx @ruvnet/rvagent invocation)
* 5. exports["." ] includes both "import" and "types" keys (ESM + types in tarball)
* 6. publishConfig.access === "public" (scoped package must be explicit)
* 7. @modelcontextprotocol/sdk is a runtime dependency (dual-transport server)
* 8. zod is a runtime dependency (input schema validation)
* 9. type === "module" (ESM-first, Node.js 20+ native)
* 10. license === "Apache-2.0"
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
// jest runs from the package root; avoid import.meta (ts-jest transforms this
// suite to a module target that rejects it — pre-existing suite failure).
const pkgPath = resolve(process.cwd(), "package.json");
// Parse once; keep raw for snapshot assertions.
const raw = readFileSync(pkgPath, "utf-8");
const pkg = JSON.parse(raw) as Record<string, unknown>;
// Helper to assert string field value.
function assertField(field: string, expected: string): void {
expect(pkg[field]).toBe(expected);
}
// Helper to get a nested value.
function nested<T>(obj: Record<string, unknown>, ...keys: string[]): T {
let cur: unknown = obj;
for (const k of keys) {
if (typeof cur !== "object" || cur === null) {
throw new Error(`Expected object at key "${k}"`);
}
cur = (cur as Record<string, unknown>)[k];
}
return cur as T;
}
describe("@ruvnet/rvagent package.json (ADR-124 §2)", () => {
it("§2.1 — name is @ruvnet/rvagent", () => {
assertField("name", "@ruvnet/rvagent");
});
it("§2.2 — version is semver >= 0.1.0", () => {
const version = pkg["version"] as string;
expect(typeof version).toBe("string");
const [major, minor] = version.split(".").map(Number);
const isAtLeast010 = (major ?? 0) > 0 || (minor ?? 0) >= 1;
expect(isAtLeast010).toBe(true);
});
it("§2.3 — engines.node requires Node.js >= 20", () => {
const nodeRange = nested<string>(pkg, "engines", "node");
expect(typeof nodeRange).toBe("string");
// Accept >=20 or >=20.0.0 patterns.
expect(nodeRange).toMatch(/>=\s*20/);
});
it("§2.4 — bin.rvagent is defined (npx @ruvnet/rvagent invocation)", () => {
const bin = nested<Record<string, string>>(pkg, "bin");
expect(typeof bin["rvagent"]).toBe("string");
expect(bin["rvagent"]).toMatch(/dist\/index\.js/);
});
it("§2.5 — exports['.'] has import + types keys (ESM + TypeScript declarations)", () => {
const exports = nested<Record<string, Record<string, string>>>(pkg, "exports");
const dotExport = exports["."];
expect(dotExport).toBeDefined();
expect(typeof dotExport?.["import"]).toBe("string");
expect(typeof dotExport?.["types"]).toBe("string");
});
it("§2.6 — publishConfig.access is 'public' (scoped package requirement)", () => {
const access = nested<string>(pkg, "publishConfig", "access");
expect(access).toBe("public");
});
it("§2.7 — @modelcontextprotocol/sdk is a runtime dependency", () => {
const deps = nested<Record<string, string>>(pkg, "dependencies");
expect(typeof deps["@modelcontextprotocol/sdk"]).toBe("string");
});
it("§2.8 — zod is a runtime dependency", () => {
const deps = nested<Record<string, string>>(pkg, "dependencies");
expect(typeof deps["zod"]).toBe("string");
});
it("§2.9 — type is 'module' (ESM-first, Node.js 20+ native)", () => {
assertField("type", "module");
});
it("§2.10 — license is Apache-2.0", () => {
assertField("license", "Apache-2.0");
});
});