Files
openclaw/test/scripts/ts-topology.test.ts
Peter Steinberger fe261b0f59 chore(tooling): typecheck root test/** with a dedicated tsgo lane (#104475)
* chore(types): add declaration files for scripts/lib and scripts/e2e modules

* chore(types): add declaration files for top-level script modules (a-m)

* chore(types): add declaration files for top-level script modules (n-z)

* test: use a non-secret-shaped gateway token fixture

* test: type ci workflow guard helpers for the root test lane

* chore(tooling): typecheck root test/** with a dedicated tsgo lane

- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
  fixtures excluded; two Docker E2E clients that import built dist/** stay out,
  same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
  the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
  now trigger 'typecheck test root' in check:changed; previously test/ paths ran
  lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
  types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
  program members; 205 sibling .d.mts declaration files for imported .mjs
  modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
  scripts/e2e/parallels/common.ts with an explicit re-export

Closes #104388

* chore(types): correct declaration fidelity per structured review

- re-derive 51 .d.mts files from implementation data flow instead of
  initializers: fix a wrong never return (runTestProjectsDelegation returns
  the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
  the full release profile union, make parsed paths string | null, add missing
  parseArgs fields via help/non-help unions, add a missing sibling declaration
  (budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
  regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
  the test-root project from tsconfig.projects.json so tsgo:all covers it
  (closes both review findings against the lane wiring)

* chore(scripts): keep telegram runner dist typing structural for the boundary guard

* chore(types): declare runtime pack and gateway readiness exports added on main

* test: pin the importTargetPlan form of the plugin-contract plan import

The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
2026-07-11 06:15:41 -07:00

225 lines
7.5 KiB
TypeScript

// Ts Topology tests cover ts topology script behavior.
import path from "node:path";
import { describe, expect, it } from "vitest";
import { analyzeTopology, filterRecordsForReport } from "../../scripts/lib/ts-topology/analyze.js";
import { renderTextReport } from "../../scripts/lib/ts-topology/reports.js";
import { createFilesystemPublicSurfaceScope } from "../../scripts/lib/ts-topology/scope.js";
import { main } from "../../scripts/ts-topology.ts";
import { createCapturedIo } from "../helpers/captured-io.js";
const repoRoot = path.join(process.cwd(), "test", "fixtures", "ts-topology", "basic");
function buildFixtureScope() {
return createFilesystemPublicSurfaceScope(repoRoot, {
id: "custom",
entrypointRoot: "src/public",
importPrefix: "fixture-sdk",
});
}
const fixtureScope = buildFixtureScope();
const publicSurfaceEnvelope = analyzeTopology({
repoRoot,
scope: fixtureScope,
report: "public-surface-usage",
});
function deriveReportEnvelope(report: Parameters<typeof filterRecordsForReport>[1]) {
return {
...publicSurfaceEnvelope,
report,
records: filterRecordsForReport(publicSurfaceEnvelope.records, report),
};
}
const singleOwnerEnvelope = deriveReportEnvelope("single-owner-shared");
const unusedEnvelope = deriveReportEnvelope("unused-public-surface");
function requireRecordByExport(exportName: string) {
const record = publicSurfaceEnvelope.records.find((entry) =>
entry.exportNames.includes(exportName),
);
if (!record) {
throw new Error(`Expected topology record for ${exportName}`);
}
return record;
}
describe("ts-topology", () => {
it("collapses canonical symbols exported by multiple public subpaths", () => {
const sharedThing = requireRecordByExport("sharedThing");
expect(sharedThing).toEqual({
aliasName: undefined,
canonicalKey: "src/lib/shared.ts:2:sharedThing",
declarationPath: "src/lib/shared.ts",
declarationLine: 2,
entrypoints: ["extra", "index"],
exportNames: ["aliasedSharedThing", "sharedThing"],
internalConsumers: [],
internalImportCount: 0,
internalRefCount: 0,
isTypeOnlyCandidate: false,
kind: "function",
moveBackToOwnerScore: 0,
productionConsumers: [
"extensions/alpha/src/use.ts",
"extensions/beta/src/use.ts",
"src/internal/use.ts",
],
productionExtensions: ["alpha", "beta"],
productionImportCount: 4,
productionPackages: ["src"],
productionRefCount: 4,
productionOwners: ["extension:alpha", "extension:beta", "src"],
publicSpecifiers: ["fixture-sdk", "fixture-sdk/extra"],
sharednessScore: 90,
testConsumers: [],
testImportCount: 0,
testRefCount: 0,
});
});
it("counts renamed imports, namespace imports, and test-only consumers without runtime-counting type-only imports", () => {
const aliasedThing = requireRecordByExport("aliasedThing");
const sharedType = requireRecordByExport("SharedType");
const testOnlyThing = requireRecordByExport("testOnlyThing");
expect(aliasedThing.productionRefCount).toBe(1);
expect(sharedType).toEqual({
aliasName: undefined,
canonicalKey: "src/lib/shared.ts:22:SharedType",
declarationLine: 22,
declarationPath: "src/lib/shared.ts",
entrypoints: ["index"],
exportNames: ["SharedType"],
internalConsumers: [],
internalImportCount: 0,
internalRefCount: 0,
isTypeOnlyCandidate: true,
kind: "type",
moveBackToOwnerScore: 20,
productionConsumers: [],
productionExtensions: [],
productionImportCount: 0,
productionOwners: [],
productionPackages: [],
productionRefCount: 0,
publicSpecifiers: ["fixture-sdk"],
sharednessScore: 15,
testConsumers: [],
testImportCount: 0,
testRefCount: 0,
});
expect(testOnlyThing).toEqual({
aliasName: undefined,
canonicalKey: "src/lib/shared.ts:14:testOnlyThing",
declarationLine: 14,
declarationPath: "src/lib/shared.ts",
entrypoints: ["index"],
exportNames: ["testOnlyThing"],
internalConsumers: [],
internalImportCount: 0,
internalRefCount: 0,
isTypeOnlyCandidate: false,
kind: "function",
moveBackToOwnerScore: 30,
productionConsumers: [],
productionExtensions: [],
productionImportCount: 0,
productionOwners: [],
productionPackages: [],
productionRefCount: 0,
publicSpecifiers: ["fixture-sdk"],
sharednessScore: 0,
testRefCount: 1,
testImportCount: 1,
testConsumers: ["tests/public.test.ts"],
});
});
it("surfaces single-owner shared and unused reports correctly", () => {
expect(singleOwnerEnvelope.records.map((record) => record.exportNames[0])).toContain(
"singleOwnerHelper",
);
expect(singleOwnerEnvelope.records.map((record) => record.exportNames[0])).not.toContain(
"sharedThing",
);
expect(unusedEnvelope.records.map((record) => record.exportNames[0])).toEqual([
"SharedType",
"unusedThing",
]);
});
it("renders stable text summaries for the public-surface report", () => {
expect(
renderTextReport({ ...publicSurfaceEnvelope, limit: 3 } as typeof publicSurfaceEnvelope, 3),
).toMatchInlineSnapshot(`
"Scope: custom
Public exports analyzed: 6
Production-used exports: 3
Single-owner shared exports: 2
Unused public exports: 2
Top 2 candidate-to-move exports:
- fixture-sdk:aliasedThing -> src/lib/shared.ts:10 (prodRefs=1, owners=extension:alpha, sharedness=35, move=85)
- fixture-sdk:singleOwnerHelper -> src/lib/shared.ts:6 (prodRefs=1, owners=extension:alpha, sharedness=35, move=85)
Top 1 duplicated public exports:
- fixture-sdk:sharedThing via fixture-sdk, fixture-sdk/extra (src/lib/shared.ts:2)"
`);
});
it("emits stable JSON through the CLI and filtered report output", async () => {
const captured = createCapturedIo();
const jsonExit = await main(
[
"--scope=custom",
"--entrypoint-root=src/public",
"--import-prefix=fixture-sdk",
"--repo-root=test/fixtures/ts-topology/basic",
"--report=single-owner-shared",
"--json",
],
captured.io,
);
expect(jsonExit).toBe(0);
const payload = JSON.parse(captured.readStdout());
expect(payload.report).toBe("single-owner-shared");
expect(
payload.records.map((record: { exportNames: string[] }) => record.exportNames[0]),
).toEqual(["aliasedThing", "singleOwnerHelper"]);
expect(renderTextReport(deriveReportEnvelope("consumer-topology"), 2)).toMatchInlineSnapshot(`
"Scope: custom
Records with consumers: 4
Top 2 consumer-topology records:
- fixture-sdk:sharedThing prod=3 test=0 internal=0
- fixture-sdk:aliasedThing prod=1 test=0 internal=0"
`);
});
it("rejects malformed CLI limits", async () => {
const captured = createCapturedIo();
const exitCode = await main(["--limit=abc"], captured.io);
expect(exitCode).toBe(1);
expect(captured.readStderr()).toContain("--limit must be a positive integer");
expect(captured.readStdout()).toBe("");
});
it("throws a clear error for invalid text report names", () => {
expect(() =>
renderTextReport(
{
...publicSurfaceEnvelope,
report: "missing-report" as typeof publicSurfaceEnvelope.report,
},
2,
),
).toThrow("Unsupported topology report: missing-report");
});
});