Skip to content

Commit

Permalink
fix(checkpoint-validation): cli fixes (langchain-ai#624)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamincburns authored Oct 24, 2024
1 parent f3d5d74 commit 74ee400
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 319 deletions.
5 changes: 3 additions & 2 deletions libs/checkpoint-validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ npm install -g @langchain/langgraph-checkpoint-validation
validate-checkpointer ./src/my_initializer.ts
```

## Usage in existing Jest test suite
## Usage in existing Jest-like test suite

If you wish to integrate this tooling into your existing Jest test suite, you import it as a library, as shown below.
This package exports a test definition function that may be used in any Jest-compatible test framework (including Vitest). If you wish to integrate this tooling into your existing test suite, you can simply import and invoke it from within a test file, as shown below.

```ts
import { validate } from "@langchain/langgraph-validation";

validate(MyCheckpointerInitializer);
```

5 changes: 1 addition & 4 deletions libs/checkpoint-validation/bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
#!/usr/bin/env node
import { register } from "node:module";
#!/usr/bin/env -S node --experimental-vm-modules
import { main } from "../dist/cli.js";

register("@swc-node/register/esm", import.meta.url);

await main();
4 changes: 1 addition & 3 deletions libs/checkpoint-validation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
"dependencies": {
"@jest/core": "^29.5.0",
"@jest/globals": "^29.5.0",
"@swc-node/register": "^1.10.9",
"@swc/core": "^1.3.90",
"@swc/jest": "^0.2.29",
"jest": "^29.5.0",
"jest-environment-node": "^29.6.4",
"ts-jest": "^29.1.0",
"uuid": "^10.0.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
Expand Down Expand Up @@ -74,8 +74,6 @@
"prettier": "^2.8.3",
"release-it": "^17.6.0",
"rollup": "^4.22.4",
"ts-jest": "^29.1.0",
"tsx": "^4.7.0",
"typescript": "^4.9.5 || ^5.4.5"
},
"publishConfig": {
Expand Down
45 changes: 32 additions & 13 deletions libs/checkpoint-validation/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import { dirname, resolve as pathResolve } from "node:path";
import { fileURLToPath } from "node:url";
import { runCLI } from "@jest/core";
import type { Config } from "@jest/types";

import { parseArgs } from "./parse_args.js";
import { validateArgs } from "./parse_args.js";

export async function main() {
const moduleDirname = dirname(fileURLToPath(import.meta.url));
const rootDir = pathResolve(
dirname(fileURLToPath(import.meta.url)),
"..",
"dist"
);
const config: Config.Argv = {
_: [pathResolve(rootDir, "runner.js")],
$0: "",
preset: "ts-jest/presets/default-esm",
rootDir,
testEnvironment: "node",
testMatch: ["<rootDir>/runner.js"],
transform: JSON.stringify({
"^.+\\.[jt]sx?$": "@swc/jest",
}),
moduleNameMapper: JSON.stringify({
"^(\\.{1,2}/.*)\\.[jt]sx?$": "$1",
}),

// jest ignores test files in node_modules by default. We want to run a test file that ships with this package, so
// we disable that behavior here.
testPathIgnorePatterns: [],
haste: JSON.stringify({
retainAllFiles: true,
}),
};

// parse here to check for errors before running Jest
await parseArgs(process.argv.slice(2));
export async function main() {
// check for argument errors before running Jest
await validateArgs(process.argv.slice(2));

await runCLI(
{
_: [],
$0: "",
runInBand: true,
},
[pathResolve(moduleDirname, "runtime_jest_config.js")]
);
await runCLI(config, [rootDir]);
}
6 changes: 6 additions & 0 deletions libs/checkpoint-validation/src/import_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export function resolveImportPath(path: string) {
// relative path
if (/^\.\.?(\/|\\)/.test(path)) {
return pathResolve(path);
} else {
const resolvedPath = pathResolve(process.cwd(), path);
// try it as a relative path, anyway
if (existsSync(resolvedPath)) {
return resolvedPath;
}
}

// module name
Expand Down
56 changes: 28 additions & 28 deletions libs/checkpoint-validation/src/parse_args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,10 @@ import {
checkpointerTestInitializerSchema,
isTestTypeFilter,
isTestTypeFilterArray,
TestTypeFilter,
testTypeFilters,
} from "./types.js";
import { dynamicImport, resolveImportPath } from "./import_utils.js";

// We have to Symbol.for here instead of unique symbols because jest gives each test file a unique module cache, so
// these symbols get created once when the jest config is evaluated, and again when runner.ts executes, making it
// impossible for runner.ts to use them as global keys.
const symbolPrefix = "langgraph-checkpoint-validation";
export const initializerSymbol = Symbol.for(`${symbolPrefix}-initializer`);
export const filtersSymbol = Symbol.for(`${symbolPrefix}-filters`);

export type ParsedArgs<
CheckpointerT extends BaseCheckpointSaver = BaseCheckpointSaver
> = {
[initializerSymbol]: CheckpointerTestInitializer<CheckpointerT>;
[filtersSymbol]: TestTypeFilter[];
};

const builder = yargs()
.command("* <initializer-import-path> [filters..]", "Validate a checkpointer")
.positional("initializerImportPath", {
Expand All @@ -48,19 +33,10 @@ const builder = yargs()

export async function parseArgs<CheckpointerT extends BaseCheckpointSaver>(
argv: string[]
): Promise<ParsedArgs<CheckpointerT>> {
) {
const { initializerImportPath, filters } = await builder.parse(argv);

let resolvedImportPath;

try {
resolvedImportPath = resolveImportPath(initializerImportPath);
} catch (e) {
console.error(
`Failed to resolve import path '${initializerImportPath}': ${e}`
);
process.exit(1);
}
const resolvedImportPath = resolveImportPath(initializerImportPath);

let initializerExport: unknown;
try {
Expand Down Expand Up @@ -97,7 +73,31 @@ export async function parseArgs<CheckpointerT extends BaseCheckpointSaver>(
}

return {
[initializerSymbol]: initializer,
[filtersSymbol]: filters,
initializer,
filters,
};
}

export async function validateArgs(argv: string[]): Promise<void> {
const { initializerImportPath, filters } = await builder.parse(argv);

try {
resolveImportPath(initializerImportPath);
} catch (e) {
console.error(
`Failed to resolve import path '${initializerImportPath}': ${e}`
);
process.exit(1);
}

if (!isTestTypeFilterArray(filters)) {
console.error(
`Invalid filters: '${filters
.filter((f) => !isTestTypeFilter(f))
.join("', '")}'. Expected only values from '${testTypeFilters.join(
"', '"
)}'`
);
process.exit(1);
}
}
9 changes: 2 additions & 7 deletions libs/checkpoint-validation/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@
// Jest test file because unfortunately there's no good way to just pass Jest a test definition function and tell it to
// run it.
import { specTest } from "./spec/index.js";
import { ParsedArgs, filtersSymbol, initializerSymbol } from "./parse_args.js";
import { parseArgs } from "./parse_args.js";

// passing via global is ugly, but there's no good alternative for handling the dynamic import here
const initializer = (globalThis as typeof globalThis & ParsedArgs)[
initializerSymbol
];
const { initializer, filters } = await parseArgs(process.argv.slice(2));

if (!initializer) {
throw new Error("Test configuration error: initializer is not set.");
}

const filters = (globalThis as typeof globalThis & ParsedArgs)[filtersSymbol];

specTest(initializer, filters);
28 changes: 0 additions & 28 deletions libs/checkpoint-validation/src/runtime_jest_config.ts

This file was deleted.

Loading

0 comments on commit 74ee400

Please sign in to comment.