From fc2c765ea6510771b292627027b474cf9d87be68 Mon Sep 17 00:00:00 2001 From: Srinivasa Consultants <78673153+dinakarjs@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:47:32 +0530 Subject: [PATCH] test(release): compile a consumer against packed npm declarations Compile an isolated strict NodeNext consumer using the lockfile-installed compiler and add CI controls for empty, missing, and widened declarations. Signed-off-by: Srinivasa Dinakar Jainti --- .github/workflows/ci.yml | 2 ++ tools/npm-consumer.mts | 24 +++++++++++++++++ tools/smoke_npm.mjs | 20 ++++++++++++-- tools/test_smoke_npm.mjs | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tools/npm-consumer.mts create mode 100644 tools/test_smoke_npm.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6196ad..391c55a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ jobs: mkdir -p ../../npm-dist npm pack --pack-destination ../../npm-dist node ../../tools/smoke_npm.mjs ../../npm-dist/*.tgz + - name: Reject broken npm declarations + run: node ../../tools/test_smoke_npm.mjs ../../npm-dist/*.tgz test: runs-on: ubuntu-latest diff --git a/tools/npm-consumer.mts b/tools/npm-consumer.mts new file mode 100644 index 0000000..8f63cd1 --- /dev/null +++ b/tools/npm-consumer.mts @@ -0,0 +1,24 @@ +// Compiled in a temporary consumer of the packed artifact, never the checkout. +import {EventFactory, EventValidationError, SchemaValidator} from "@agentrust-io/telemetry"; +import type {EnvelopeFields, NormalizedEvent} from "@agentrust-io/telemetry"; + +const validator: SchemaValidator = SchemaValidator.bundled(); +const fields: EnvelopeFields = {runId: "run-1", timeUnixNano: 1787079000000000000n}; +const factory = new EventFactory(validator, {name: "consumer", version: "1.0.0"}); +const event: NormalizedEvent = factory.build("usage.recorded", fields, + {scope: "model_call", operation: "chat", input_tokens: 7}); +const runId: string = event.run_id; +const candidate: unknown = event; +validator.validate(candidate); +const validated: NormalizedEvent = candidate; +const error: Error = new EventValidationError("invalid event"); +void [runId, validated, error]; + +// Guard against declarations silently widening the consumer API to any. +// @ts-expect-error runId must be a string +factory.build("usage.recorded", {runId: 42}, {}); +// @ts-expect-error the envelope requires runId +factory.build("usage.recorded", {}, {}); +// @ts-expect-error normalized event IDs are strings +const numericRunId: number = event.run_id; +void numericRunId; diff --git a/tools/smoke_npm.mjs b/tools/smoke_npm.mjs index 0ff0c49..8e40a79 100644 --- a/tools/smoke_npm.mjs +++ b/tools/smoke_npm.mjs @@ -1,4 +1,4 @@ -// Exercise the packed consumer surface outside the checkout, without dev tools. +// Exercise the packed consumer surface outside the checkout. import assert from "node:assert/strict"; import {execFileSync} from "node:child_process"; import {mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync} from "node:fs"; @@ -60,7 +60,23 @@ test("installed event construction matches the golden control", () => { {scope: "model_call", operation: "chat", input_tokens: 7}), golden); }); `); - execFileSync(process.execPath, ["--test", "smoke.test.mjs"], {cwd: directory, stdio: "inherit"}); + execFileSync(process.execPath, ["--test", "--test-reporter=tap", "smoke.test.mjs"], + {cwd: directory, stdio: "inherit"}); + // Use the compiler installed by npm ci (and pinned by package-lock.json), + // but resolve SDK imports only from the isolated consumer's node_modules. + writeFileSync(join(directory, "consumer.mts"), + readFileSync(join(root, "tools/npm-consumer.mts"), "utf8")); + writeFileSync(join(directory, "tsconfig.json"), JSON.stringify({ + compilerOptions: { + target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", + strict: true, noEmit: true, skipLibCheck: false, types: [], + }, + files: ["consumer.mts"], + })); + execFileSync(process.execPath, + [join(root, "packages/typescript/node_modules/typescript/bin/tsc"), "-p", "tsconfig.json"], + {cwd: directory, stdio: "inherit"}); + console.log("Installed TypeScript consumer compiled successfully"); } finally { rmSync(directory, {recursive: true, force: true}); } diff --git a/tools/test_smoke_npm.mjs b/tools/test_smoke_npm.mjs new file mode 100644 index 0000000..ad5e87d --- /dev/null +++ b/tools/test_smoke_npm.mjs @@ -0,0 +1,56 @@ +// Regression controls: JavaScript remains intact while declarations are broken. +import assert from "node:assert/strict"; +import {execFileSync, spawnSync} from "node:child_process"; +import {mkdtempSync, readFileSync, rmSync, writeFileSync} from "node:fs"; +import {tmpdir} from "node:os"; +import {join, resolve} from "node:path"; +import test from "node:test"; +import {fileURLToPath} from "node:url"; + +assert.equal(process.argv.length, 3, "usage: node tools/test_smoke_npm.mjs distribution.tgz"); +const distribution = resolve(process.argv[2]); +const smoke = fileURLToPath(new URL("smoke_npm.mjs", import.meta.url)); +const controls = [ + { + name: "empty declaration exports", diagnostic: /TS2305/, + mutate(directory) { + writeFileSync(join(directory, "package/dist/index.d.ts"), "export {};\n"); + }, + }, + { + name: "missing transitive declaration", diagnostic: /TS7016/, + mutate(directory) { + rmSync(join(directory, "package/dist/types.d.ts")); + }, + }, + { + name: "widened input type", diagnostic: /TS2578/, + mutate(directory) { + const path = join(directory, "package/dist/factory.d.ts"); + const original = readFileSync(path, "utf8"); + assert.match(original, /runId: string;/); + writeFileSync(path, original.replace("runId: string;", "runId: any;")); + }, + }, +]; + +for (const control of controls) { + test(`packed consumer rejects ${control.name}`, () => { + const directory = mkdtempSync(join(tmpdir(), "agentrust-npm-regression-")); + try { + execFileSync("tar", ["-xzf", distribution, "-C", directory]); + control.mutate(directory); + const mutant = join(directory, "mutant.tgz"); + execFileSync("tar", ["-czf", mutant, "-C", directory, "package"]); + const result = spawnSync(process.execPath, [smoke, mutant], {encoding: "utf8"}); + assert.ifError(result.error); + const output = result.stdout + result.stderr; + // Confirm failure is in declaration compilation, not install/runtime. + assert.match(output, /# pass 4/, output); + assert.equal(result.status, 1, output); + assert.match(output, control.diagnostic, output); + } finally { + rmSync(directory, {recursive: true, force: true}); + } + }); +}