From 1444f62a2115827c3d4af4c9203c59215b2449d5 Mon Sep 17 00:00:00 2001 From: Yuta Kasai Date: Sat, 9 May 2026 12:41:18 +0900 Subject: [PATCH 1/5] test dual package --- .npmrc | 1 + .../ts-modern/files/src/smoke-resolution.ts | 10 ++ .../ts-modern/package.commonjs.json | 12 ++ .../blueprints/ts-modern/package.module.json | 12 ++ .../ts-modern/tsconfig.bundler.json | 13 ++ .../ts-modern/tsconfig.legacy-commonjs.json | 13 ++ .../ts-modern/tsconfig.nodenext.json | 13 ++ test/consumer/fixtures/js-cjs/package.json | 5 + .../fixtures/js-cjs/smoke-outbound.cjs | 117 ++++++++++++++ .../fixtures/js-cjs/smoke-resolution.cjs | 10 ++ .../fixtures/js-cjs/smoke-webhook.cjs | 73 +++++++++ test/consumer/fixtures/js-esm/package.json | 5 + .../fixtures/js-esm/smoke-outbound.mjs | 104 +++++++++++++ .../fixtures/js-esm/smoke-resolution.mjs | 10 ++ .../fixtures/js-esm/smoke-webhook.mjs | 66 ++++++++ test/consumer/harness/runner.ts | 143 ++++++++++++++++++ test/consumer/smoke-js.spec.ts | 50 ++++++ test/consumer/smoke-packaging.spec.ts | 61 ++++++++ test/consumer/smoke-ts5.spec.ts | 89 +++++++++++ test/consumer/smoke-ts6.spec.ts | 74 +++++++++ 20 files changed, 881 insertions(+) create mode 100644 test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts create mode 100644 test/consumer/blueprints/ts-modern/package.commonjs.json create mode 100644 test/consumer/blueprints/ts-modern/package.module.json create mode 100644 test/consumer/blueprints/ts-modern/tsconfig.bundler.json create mode 100644 test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json create mode 100644 test/consumer/blueprints/ts-modern/tsconfig.nodenext.json create mode 100644 test/consumer/fixtures/js-cjs/package.json create mode 100644 test/consumer/fixtures/js-cjs/smoke-outbound.cjs create mode 100644 test/consumer/fixtures/js-cjs/smoke-resolution.cjs create mode 100644 test/consumer/fixtures/js-cjs/smoke-webhook.cjs create mode 100644 test/consumer/fixtures/js-esm/package.json create mode 100644 test/consumer/fixtures/js-esm/smoke-outbound.mjs create mode 100644 test/consumer/fixtures/js-esm/smoke-resolution.mjs create mode 100644 test/consumer/fixtures/js-esm/smoke-webhook.mjs create mode 100644 test/consumer/harness/runner.ts create mode 100644 test/consumer/smoke-js.spec.ts create mode 100644 test/consumer/smoke-packaging.spec.ts create mode 100644 test/consumer/smoke-ts5.spec.ts create mode 100644 test/consumer/smoke-ts6.spec.ts diff --git a/.npmrc b/.npmrc index 2143f3df2..ddf24ae25 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ ignore-scripts=true min-release-age=7 +registry=https://registry.npmjs.org diff --git a/test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts b/test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts new file mode 100644 index 000000000..074ad257e --- /dev/null +++ b/test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts @@ -0,0 +1,10 @@ +import * as assert from "node:assert/strict"; +import { LineBotClient, middleware } from "@line/bot-sdk"; + +assert.equal(typeof middleware, "function"); +assert.equal(typeof LineBotClient.fromChannelAccessToken, "function"); + +const client = LineBotClient.fromChannelAccessToken({ + channelAccessToken: "DUMMY_TOKEN", +}); +assert.equal(typeof client.getBotInfo, "function"); diff --git a/test/consumer/blueprints/ts-modern/package.commonjs.json b/test/consumer/blueprints/ts-modern/package.commonjs.json new file mode 100644 index 000000000..5b1c3b6c6 --- /dev/null +++ b/test/consumer/blueprints/ts-modern/package.commonjs.json @@ -0,0 +1,12 @@ +{ + "name": "consumer-ts-modern-cjs", + "private": true, + "type": "commonjs", + "devDependencies": { + "typescript": "__TS_VERSION__" + }, + "scripts": { + "build": "tsc -p ./tsconfig.json", + "run:resolution": "node ./dist/smoke-resolution.js" + } +} diff --git a/test/consumer/blueprints/ts-modern/package.module.json b/test/consumer/blueprints/ts-modern/package.module.json new file mode 100644 index 000000000..a6adda459 --- /dev/null +++ b/test/consumer/blueprints/ts-modern/package.module.json @@ -0,0 +1,12 @@ +{ + "name": "consumer-ts-modern-esm", + "private": true, + "type": "module", + "devDependencies": { + "typescript": "__TS_VERSION__" + }, + "scripts": { + "build": "tsc -p ./tsconfig.json", + "run:resolution": "node ./dist/smoke-resolution.js" + } +} diff --git a/test/consumer/blueprints/ts-modern/tsconfig.bundler.json b/test/consumer/blueprints/ts-modern/tsconfig.bundler.json new file mode 100644 index 000000000..dbb1d841c --- /dev/null +++ b/test/consumer/blueprints/ts-modern/tsconfig.bundler.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": false, + "types": ["node"], + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json b/test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json new file mode 100644 index 000000000..cb76c687e --- /dev/null +++ b/test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "strict": true, + "skipLibCheck": false, + "types": ["node"], + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/test/consumer/blueprints/ts-modern/tsconfig.nodenext.json b/test/consumer/blueprints/ts-modern/tsconfig.nodenext.json new file mode 100644 index 000000000..3b39615e5 --- /dev/null +++ b/test/consumer/blueprints/ts-modern/tsconfig.nodenext.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": false, + "types": ["node"], + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/test/consumer/fixtures/js-cjs/package.json b/test/consumer/fixtures/js-cjs/package.json new file mode 100644 index 000000000..7dbeb4f06 --- /dev/null +++ b/test/consumer/fixtures/js-cjs/package.json @@ -0,0 +1,5 @@ +{ + "name": "consumer-js-cjs", + "private": true, + "type": "commonjs" +} diff --git a/test/consumer/fixtures/js-cjs/smoke-outbound.cjs b/test/consumer/fixtures/js-cjs/smoke-outbound.cjs new file mode 100644 index 000000000..d4a049173 --- /dev/null +++ b/test/consumer/fixtures/js-cjs/smoke-outbound.cjs @@ -0,0 +1,117 @@ +const assert = require("node:assert/strict"); +const { createServer } = require("node:http"); +const { once } = require("node:events"); +const { LineBotClient } = require("@line/bot-sdk"); + +const requests = []; +const server = createServer(async (req, res) => { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + const body = Buffer.concat(chunks).toString("utf8"); + requests.push({ + method: req.method, + url: req.url, + auth: req.headers.authorization, + body, + }); + + if (req.method === "GET" && req.url === "/v2/bot/info") { + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + userId: "U1234567890", + basicId: "@testbot", + displayName: "Test Bot", + }), + ); + return; + } + + if ( + req.method === "GET" && + req.url === "/v2/bot/message/message-1/content/transcoding" + ) { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ status: "succeeded" })); + return; + } + + if (req.method === "POST" && req.url === "/module/auth/v1/token") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ access_token: "module-token" })); + return; + } + + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ message: "Not Found" })); +}); + +async function main() { + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("failed to determine server address"); + } + + const baseUrl = `http://127.0.0.1:${address.port}`; + + try { + const client = LineBotClient.fromChannelAccessToken({ + channelAccessToken: "DUMMY_TOKEN", + apiBaseURL: baseUrl, + dataApiBaseURL: baseUrl, + managerBaseURL: baseUrl, + }); + + const botInfo = await client.getBotInfo(); + assert.equal(botInfo.displayName, "Test Bot"); + + const transcoding = await client.getMessageContentTranscodingByMessageId( + "message-1", + ); + assert.equal(transcoding.status, "succeeded"); + + await client.attachModule( + "authorization_code", + "code-1", + "https://example.com/callback", + ); + + assert.equal(requests.length, 3); + + assert.deepEqual( + requests.map((request) => ({ method: request.method, url: request.url })), + [ + { method: "GET", url: "/v2/bot/info" }, + { + method: "GET", + url: "/v2/bot/message/message-1/content/transcoding", + }, + { method: "POST", url: "/module/auth/v1/token" }, + ], + ); + + for (const request of requests) { + assert.equal(request.auth, "Bearer DUMMY_TOKEN"); + } + + const formBody = requests[2]?.body ?? ""; + assert.match(formBody, /grant_type=authorization_code/); + assert.match(formBody, /code=code-1/); + assert.match( + formBody, + /redirect_uri=https%3A%2F%2Fexample.com%2Fcallback/, + ); + } finally { + server.close(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/consumer/fixtures/js-cjs/smoke-resolution.cjs b/test/consumer/fixtures/js-cjs/smoke-resolution.cjs new file mode 100644 index 000000000..9fae3c8a8 --- /dev/null +++ b/test/consumer/fixtures/js-cjs/smoke-resolution.cjs @@ -0,0 +1,10 @@ +const assert = require("node:assert/strict"); +const sdk = require("@line/bot-sdk"); + +assert.equal(typeof sdk.middleware, "function"); +assert.equal(typeof sdk.LineBotClient.fromChannelAccessToken, "function"); + +const client = sdk.LineBotClient.fromChannelAccessToken({ + channelAccessToken: "DUMMY_TOKEN", +}); +assert.equal(typeof client.getBotInfo, "function"); diff --git a/test/consumer/fixtures/js-cjs/smoke-webhook.cjs b/test/consumer/fixtures/js-cjs/smoke-webhook.cjs new file mode 100644 index 000000000..afdbbe8f0 --- /dev/null +++ b/test/consumer/fixtures/js-cjs/smoke-webhook.cjs @@ -0,0 +1,73 @@ +const assert = require("node:assert/strict"); +const { createHmac } = require("node:crypto"); +const { createServer } = require("node:http"); +const { once } = require("node:events"); +const { middleware } = require("@line/bot-sdk"); + +const channelSecret = "channel-secret"; + +const makeSignature = (body) => { + return createHmac("sha256", channelSecret).update(body).digest("base64"); +}; + +const mw = middleware({ channelSecret }); + +const server = createServer((req, res) => { + mw(req, res, (err) => { + if (err) { + res.statusCode = 401; + res.end("invalid"); + return; + } + + assert.equal(Array.isArray(req.body.events), true); + res.statusCode = 200; + res.end("ok"); + }); +}); + +async function main() { + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("failed to determine server address"); + } + + const url = `http://127.0.0.1:${address.port}/webhook`; + + const payload = JSON.stringify({ + destination: "UDEST", + events: [{ type: "message", message: { type: "text", text: "hi" } }], + }); + + try { + const okRes = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-line-signature": makeSignature(payload), + }, + body: payload, + }); + assert.equal(okRes.status, 200); + + const ngRes = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-line-signature": "invalid-signature", + }, + body: payload, + }); + assert.equal(ngRes.status, 401); + } finally { + server.close(); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/consumer/fixtures/js-esm/package.json b/test/consumer/fixtures/js-esm/package.json new file mode 100644 index 000000000..d3042e00f --- /dev/null +++ b/test/consumer/fixtures/js-esm/package.json @@ -0,0 +1,5 @@ +{ + "name": "consumer-js-esm", + "private": true, + "type": "module" +} diff --git a/test/consumer/fixtures/js-esm/smoke-outbound.mjs b/test/consumer/fixtures/js-esm/smoke-outbound.mjs new file mode 100644 index 000000000..2d907f164 --- /dev/null +++ b/test/consumer/fixtures/js-esm/smoke-outbound.mjs @@ -0,0 +1,104 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:http"; +import { once } from "node:events"; +import { LineBotClient } from "@line/bot-sdk"; + +const requests = []; +const server = createServer(async (req, res) => { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + const body = Buffer.concat(chunks).toString("utf8"); + requests.push({ + method: req.method, + url: req.url, + auth: req.headers.authorization, + body, + }); + + if (req.method === "GET" && req.url === "/v2/bot/info") { + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + userId: "U1234567890", + basicId: "@testbot", + displayName: "Test Bot", + }), + ); + return; + } + + if ( + req.method === "GET" && + req.url === "/v2/bot/message/message-1/content/transcoding" + ) { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ status: "succeeded" })); + return; + } + + if (req.method === "POST" && req.url === "/module/auth/v1/token") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ access_token: "module-token" })); + return; + } + + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ message: "Not Found" })); +}); + +server.listen(0, "127.0.0.1"); +await once(server, "listening"); + +const address = server.address(); +if (!address || typeof address === "string") { + throw new Error("failed to determine server address"); +} + +const baseUrl = `http://127.0.0.1:${address.port}`; + +try { + const client = LineBotClient.fromChannelAccessToken({ + channelAccessToken: "DUMMY_TOKEN", + apiBaseURL: baseUrl, + dataApiBaseURL: baseUrl, + managerBaseURL: baseUrl, + }); + + const botInfo = await client.getBotInfo(); + assert.equal(botInfo.displayName, "Test Bot"); + + const transcoding = await client.getMessageContentTranscodingByMessageId( + "message-1", + ); + assert.equal(transcoding.status, "succeeded"); + + await client.attachModule( + "authorization_code", + "code-1", + "https://example.com/callback", + ); + + assert.equal(requests.length, 3); + + assert.deepEqual( + requests.map((request) => ({ method: request.method, url: request.url })), + [ + { method: "GET", url: "/v2/bot/info" }, + { method: "GET", url: "/v2/bot/message/message-1/content/transcoding" }, + { method: "POST", url: "/module/auth/v1/token" }, + ], + ); + + for (const request of requests) { + assert.equal(request.auth, "Bearer DUMMY_TOKEN"); + } + + const formBody = requests[2]?.body ?? ""; + assert.match(formBody, /grant_type=authorization_code/); + assert.match(formBody, /code=code-1/); + assert.match(formBody, /redirect_uri=https%3A%2F%2Fexample.com%2Fcallback/); +} finally { + server.close(); +} diff --git a/test/consumer/fixtures/js-esm/smoke-resolution.mjs b/test/consumer/fixtures/js-esm/smoke-resolution.mjs new file mode 100644 index 000000000..b6a865c2f --- /dev/null +++ b/test/consumer/fixtures/js-esm/smoke-resolution.mjs @@ -0,0 +1,10 @@ +import assert from "node:assert/strict"; +import { LineBotClient, middleware } from "@line/bot-sdk"; + +assert.equal(typeof middleware, "function"); +assert.equal(typeof LineBotClient.fromChannelAccessToken, "function"); + +const client = LineBotClient.fromChannelAccessToken({ + channelAccessToken: "DUMMY_TOKEN", +}); +assert.equal(typeof client.getBotInfo, "function"); diff --git a/test/consumer/fixtures/js-esm/smoke-webhook.mjs b/test/consumer/fixtures/js-esm/smoke-webhook.mjs new file mode 100644 index 000000000..fb901e875 --- /dev/null +++ b/test/consumer/fixtures/js-esm/smoke-webhook.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { createHmac } from "node:crypto"; +import { createServer } from "node:http"; +import { once } from "node:events"; +import { middleware } from "@line/bot-sdk"; + +const channelSecret = "channel-secret"; + +const makeSignature = (body) => { + return createHmac("sha256", channelSecret).update(body).digest("base64"); +}; + +const mw = middleware({ channelSecret }); + +const server = createServer((req, res) => { + mw(req, res, (err) => { + if (err) { + res.statusCode = 401; + res.end("invalid"); + return; + } + + assert.equal(Array.isArray(req.body.events), true); + res.statusCode = 200; + res.end("ok"); + }); +}); + +server.listen(0, "127.0.0.1"); +await once(server, "listening"); + +const address = server.address(); +if (!address || typeof address === "string") { + throw new Error("failed to determine server address"); +} + +const url = `http://127.0.0.1:${address.port}/webhook`; + +const payload = JSON.stringify({ + destination: "UDEST", + events: [{ type: "message", message: { type: "text", text: "hi" } }], +}); + +try { + const okRes = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-line-signature": makeSignature(payload), + }, + body: payload, + }); + assert.equal(okRes.status, 200); + + const ngRes = await fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + "x-line-signature": "invalid-signature", + }, + body: payload, + }); + assert.equal(ngRes.status, 401); +} finally { + server.close(); +} diff --git a/test/consumer/harness/runner.ts b/test/consumer/harness/runner.ts new file mode 100644 index 000000000..7f85b6218 --- /dev/null +++ b/test/consumer/harness/runner.ts @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +export interface CmdResult { + readonly stdout: string; + readonly stderr: string; +} + +export function runCommand( + cwd: string, + command: string, + args: string[], + env: Record = {}, +): CmdResult { + const defaultEnv = + command === "npm" && env.NPM_CONFIG_CACHE == null + ? { + NPM_CONFIG_CACHE: path.join(os.tmpdir(), "bot-sdk-npm-cache"), + NPM_CONFIG_REGISTRY: "https://registry.npmjs.org", + } + : {}; + + const result = spawnSync(command, args, { + cwd, + env: { ...process.env, ...defaultEnv, ...env }, + encoding: "utf8", + }); + + if (result.status !== 0) { + const joined = [ + `$ ${command} ${args.join(" ")}`, + `cwd: ${cwd}`, + result.stdout, + result.stderr, + ] + .filter(Boolean) + .join("\n"); + throw new Error(joined); + } + + return { + stdout: result.stdout, + stderr: result.stderr, + }; +} + +export async function createTempDir(prefix: string): Promise { + return mkdtemp(path.join(os.tmpdir(), prefix)); +} + +export async function removeDir(dirPath: string): Promise { + await rm(dirPath, { recursive: true, force: true }); +} + +export async function copyDir(src: string, dst: string): Promise { + await cp(src, dst, { recursive: true }); +} + +export async function buildPackedTarball(repoRoot: string): Promise { + const { stdout } = runCommand(repoRoot, "npm", ["pack", "--json"]); + const parsed = JSON.parse(stdout) as Array<{ filename: string }>; + const filename = parsed[0]?.filename; + assert.ok(filename, "npm pack --json did not return filename"); + return path.join(repoRoot, filename); +} + +export async function readTarPackageJson( + repoRoot: string, + tarballPath: string, +): Promise { + const { stdout } = runCommand(repoRoot, "tar", [ + "-xOf", + tarballPath, + "package/package.json", + ]); + return JSON.parse(stdout); +} + +export function listTarEntries( + repoRoot: string, + tarballPath: string, +): string[] { + const { stdout } = runCommand(repoRoot, "tar", ["-tf", tarballPath]); + return stdout + .split("\n") + .map(line => line.trim()) + .filter(line => line.length > 0); +} + +export async function materializeTsFixture(params: { + readonly repoRoot: string; + readonly outDir: string; + readonly packageTemplateFile: string; + readonly tsconfigTemplateFile?: string; + readonly tsVersion: string; +}): Promise { + const blueprintRoot = path.join( + params.repoRoot, + "test/consumer/blueprints/ts-modern", + ); + await copyDir(path.join(blueprintRoot, "files"), params.outDir); + + const packageTemplate = await readFile( + path.join(blueprintRoot, params.packageTemplateFile), + "utf8", + ); + const packageJson = packageTemplate.replace( + "__TS_VERSION__", + params.tsVersion, + ); + await writeFile( + path.join(params.outDir, "package.json"), + packageJson, + "utf8", + ); + + const tsconfig = await readFile( + path.join( + blueprintRoot, + params.tsconfigTemplateFile ?? "tsconfig.nodenext.json", + ), + "utf8", + ); + await writeFile(path.join(params.outDir, "tsconfig.json"), tsconfig, "utf8"); +} + +export function installSdkTarball( + projectDir: string, + tarballPath: string, +): void { + runCommand(projectDir, "npm", [ + "install", + "--no-audit", + "--no-fund", + "--ignore-scripts", + "--prefer-offline", + "--no-save", + `file:${tarballPath}`, + ]); +} diff --git a/test/consumer/smoke-js.spec.ts b/test/consumer/smoke-js.spec.ts new file mode 100644 index 000000000..a17b16a90 --- /dev/null +++ b/test/consumer/smoke-js.spec.ts @@ -0,0 +1,50 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + copyDir, + createTempDir, + installSdkTarball, + removeDir, + runCommand, +} from "./harness/runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package JS consumer smoke", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it("runs JS ESM fixture with resolution and behavioral smoke", async () => { + const fixtureDir = await prepareFixtureDir("js-esm"); + await copyDir(`${repoRoot}/test/consumer/fixtures/js-esm`, fixtureDir); + + installSdkTarball(fixtureDir, tarballPath); + + runCommand(fixtureDir, "node", ["./smoke-resolution.mjs"]); + runCommand(fixtureDir, "node", ["./smoke-outbound.mjs"]); + runCommand(fixtureDir, "node", ["./smoke-webhook.mjs"]); + }, 180_000); + + it("runs JS CJS fixture resolution smoke", async () => { + const fixtureDir = await prepareFixtureDir("js-cjs"); + await copyDir(`${repoRoot}/test/consumer/fixtures/js-cjs`, fixtureDir); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "node", ["./smoke-resolution.cjs"]); + runCommand(fixtureDir, "node", ["./smoke-outbound.cjs"]); + runCommand(fixtureDir, "node", ["./smoke-webhook.cjs"]); + }, 120_000); +}); diff --git a/test/consumer/smoke-packaging.spec.ts b/test/consumer/smoke-packaging.spec.ts new file mode 100644 index 000000000..86d2b7d51 --- /dev/null +++ b/test/consumer/smoke-packaging.spec.ts @@ -0,0 +1,61 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + listTarEntries, + readTarPackageJson, +} from "./harness/runner"; + +const repoRoot = process.cwd(); +let tarballPath = ""; + +describe("dual package packaging smoke", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + assert.equal(existsSync(tarballPath), true); + }); + + it("includes required export targets and metadata", async () => { + const entries = listTarEntries(repoRoot, tarballPath); + const requiredEntries = [ + "package/dist/index.js", + "package/dist/index.d.ts", + "package/dist/cjs/index.js", + "package/dist/cjs/index.d.ts", + "package/dist/cjs/package.json", + "package/package.json", + ]; + + for (const entry of requiredEntries) { + assert.equal( + entries.includes(entry), + true, + `${entry} was not found in tarball`, + ); + } + + const packagedJson = await readTarPackageJson(repoRoot, tarballPath); + assert.equal(packagedJson.main, "./dist/cjs/index.js"); + assert.equal(packagedJson.types, "./dist/index.d.ts"); + + assert.equal(packagedJson.exports["."].import.types, "./dist/index.d.ts"); + assert.equal(packagedJson.exports["."].import.default, "./dist/index.js"); + assert.equal( + packagedJson.exports["."].require.types, + "./dist/cjs/index.d.ts", + ); + assert.equal( + packagedJson.exports["."].require.default, + "./dist/cjs/index.js", + ); + }); + + it("does not include test artifacts", () => { + const entries = listTarEntries(repoRoot, tarballPath); + const testArtifacts = entries.filter(entry => + /(^|\/)tests?\//.test(entry.replace(/^package\//, "")), + ); + assert.deepEqual(testArtifacts, []); + }); +}); diff --git a/test/consumer/smoke-ts5.spec.ts b/test/consumer/smoke-ts5.spec.ts new file mode 100644 index 000000000..d08c4c23b --- /dev/null +++ b/test/consumer/smoke-ts5.spec.ts @@ -0,0 +1,89 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + createTempDir, + installSdkTarball, + materializeTsFixture, + removeDir, + runCommand, +} from "./harness/runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; +const TS5_RANGE = ">=5.5.4 <6"; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package TS5 consumer smoke", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it(`runs TS ESM modern lane (${TS5_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts5-esm-modern"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + tsVersion: TS5_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + }, 240_000); + + it(`runs TS CJS modern lane (${TS5_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts5-cjs-modern"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + tsVersion: TS5_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + }, 240_000); + + it(`runs TS CJS legacy lane (${TS5_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts5-cjs-legacy"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.legacy-commonjs.json", + tsVersion: TS5_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + }, 240_000); + + it(`runs TS bundler compile-only lane (${TS5_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts5-bundler"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + tsVersion: TS5_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + }, 240_000); +}); diff --git a/test/consumer/smoke-ts6.spec.ts b/test/consumer/smoke-ts6.spec.ts new file mode 100644 index 000000000..2ddb57101 --- /dev/null +++ b/test/consumer/smoke-ts6.spec.ts @@ -0,0 +1,74 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + createTempDir, + installSdkTarball, + materializeTsFixture, + removeDir, + runCommand, +} from "./harness/runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; +const TS6_RANGE = ">=6.0.0 <7"; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package TS6 consumer smoke", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it(`runs TS ESM modern lane (${TS6_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts6-esm-modern"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + tsVersion: TS6_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + }, 240_000); + + it(`runs TS CJS modern lane (${TS6_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts6-cjs-modern"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + tsVersion: TS6_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + }, 240_000); + + it(`runs TS bundler compile-only lane (${TS6_RANGE})`, async () => { + const fixtureDir = await prepareFixtureDir("ts6-bundler"); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + tsVersion: TS6_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + }, 240_000); +}); From de5c5bfcbf8579ec3b845a47d74232996b351a52 Mon Sep 17 00:00:00 2001 From: Yuta Kasai Date: Sat, 9 May 2026 12:54:55 +0900 Subject: [PATCH 2/5] polish --- test/consumer/consumer-contract-js.spec.ts | 70 +++++++++++ ...ts => consumer-contract-packaging.spec.ts} | 4 +- test/consumer/consumer-contract-ts5.spec.ts | 110 ++++++++++++++++++ test/consumer/consumer-contract-ts6.spec.ts | 96 +++++++++++++++ ...utbound.cjs => outbound-http-contract.cjs} | 0 ...solution.cjs => resolution-load-check.cjs} | 0 ...ook.cjs => webhook-signature-contract.cjs} | 0 ...utbound.mjs => outbound-http-contract.mjs} | 0 ...solution.mjs => resolution-load-check.mjs} | 0 ...ook.mjs => webhook-signature-contract.mjs} | 0 .../{harness/runner.ts => runner/index.ts} | 10 +- test/consumer/smoke-js.spec.ts | 50 -------- test/consumer/smoke-ts5.spec.ts | 89 -------------- test/consumer/smoke-ts6.spec.ts | 74 ------------ .../files/src/resolution-load-check.ts} | 0 .../ts-modern/package.commonjs.json | 2 +- .../ts-modern/package.module.json | 2 +- .../ts-modern/tsconfig.bundler.json | 0 .../ts-modern/tsconfig.legacy-commonjs.json | 0 .../ts-modern/tsconfig.nodenext.json | 0 20 files changed, 285 insertions(+), 222 deletions(-) create mode 100644 test/consumer/consumer-contract-js.spec.ts rename test/consumer/{smoke-packaging.spec.ts => consumer-contract-packaging.spec.ts} (95%) create mode 100644 test/consumer/consumer-contract-ts5.spec.ts create mode 100644 test/consumer/consumer-contract-ts6.spec.ts rename test/consumer/fixtures/js-cjs/{smoke-outbound.cjs => outbound-http-contract.cjs} (100%) rename test/consumer/fixtures/js-cjs/{smoke-resolution.cjs => resolution-load-check.cjs} (100%) rename test/consumer/fixtures/js-cjs/{smoke-webhook.cjs => webhook-signature-contract.cjs} (100%) rename test/consumer/fixtures/js-esm/{smoke-outbound.mjs => outbound-http-contract.mjs} (100%) rename test/consumer/fixtures/js-esm/{smoke-resolution.mjs => resolution-load-check.mjs} (100%) rename test/consumer/fixtures/js-esm/{smoke-webhook.mjs => webhook-signature-contract.mjs} (100%) rename test/consumer/{harness/runner.ts => runner/index.ts} (93%) delete mode 100644 test/consumer/smoke-js.spec.ts delete mode 100644 test/consumer/smoke-ts5.spec.ts delete mode 100644 test/consumer/smoke-ts6.spec.ts rename test/consumer/{blueprints/ts-modern/files/src/smoke-resolution.ts => templates/ts-modern/files/src/resolution-load-check.ts} (100%) rename test/consumer/{blueprints => templates}/ts-modern/package.commonjs.json (76%) rename test/consumer/{blueprints => templates}/ts-modern/package.module.json (76%) rename test/consumer/{blueprints => templates}/ts-modern/tsconfig.bundler.json (100%) rename test/consumer/{blueprints => templates}/ts-modern/tsconfig.legacy-commonjs.json (100%) rename test/consumer/{blueprints => templates}/ts-modern/tsconfig.nodenext.json (100%) diff --git a/test/consumer/consumer-contract-js.spec.ts b/test/consumer/consumer-contract-js.spec.ts new file mode 100644 index 000000000..db440f232 --- /dev/null +++ b/test/consumer/consumer-contract-js.spec.ts @@ -0,0 +1,70 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + copyDir, + createTempDir, + installSdkTarball, + removeDir, + runCommand, +} from "./runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; +const JS_TIMEOUT_MS = 180_000; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +async function runJsFixture( + fixtureName: "js-esm" | "js-cjs", + scripts: readonly string[], +): Promise { + const fixtureDir = await prepareFixtureDir(fixtureName); + await copyDir( + `${repoRoot}/test/consumer/fixtures/${fixtureName}`, + fixtureDir, + ); + installSdkTarball(fixtureDir, tarballPath); + + for (const script of scripts) { + runCommand(fixtureDir, "node", [script]); + } +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package JS consumer contract", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it( + "runs JS ESM fixture with resolution and behavioral contract checks", + async () => { + await runJsFixture("js-esm", [ + "./resolution-load-check.mjs", + "./outbound-http-contract.mjs", + "./webhook-signature-contract.mjs", + ]); + }, + JS_TIMEOUT_MS, + ); + + it( + "runs JS CJS fixture resolution and behavioral contract checks", + async () => { + await runJsFixture("js-cjs", [ + "./resolution-load-check.cjs", + "./outbound-http-contract.cjs", + "./webhook-signature-contract.cjs", + ]); + }, + JS_TIMEOUT_MS, + ); +}); diff --git a/test/consumer/smoke-packaging.spec.ts b/test/consumer/consumer-contract-packaging.spec.ts similarity index 95% rename from test/consumer/smoke-packaging.spec.ts rename to test/consumer/consumer-contract-packaging.spec.ts index 86d2b7d51..58c88ca1c 100644 --- a/test/consumer/smoke-packaging.spec.ts +++ b/test/consumer/consumer-contract-packaging.spec.ts @@ -5,12 +5,12 @@ import { buildPackedTarball, listTarEntries, readTarPackageJson, -} from "./harness/runner"; +} from "./runner"; const repoRoot = process.cwd(); let tarballPath = ""; -describe("dual package packaging smoke", () => { +describe("dual package packaging contract", () => { beforeAll(async () => { tarballPath = await buildPackedTarball(repoRoot); assert.equal(existsSync(tarballPath), true); diff --git a/test/consumer/consumer-contract-ts5.spec.ts b/test/consumer/consumer-contract-ts5.spec.ts new file mode 100644 index 000000000..382a9cfcb --- /dev/null +++ b/test/consumer/consumer-contract-ts5.spec.ts @@ -0,0 +1,110 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + createTempDir, + installSdkTarball, + materializeTsFixture, + removeDir, + runCommand, +} from "./runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; +const TS5_RANGE = ">=5.5.4 <6"; +const TS_TIMEOUT_MS = 240_000; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +interface TsLaneConfig { + readonly fixtureName: string; + readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; + readonly tsconfigTemplateFile: + | "tsconfig.nodenext.json" + | "tsconfig.legacy-commonjs.json" + | "tsconfig.bundler.json"; + readonly withRuntime: boolean; +} + +async function runTsLane(config: TsLaneConfig): Promise { + const fixtureDir = await prepareFixtureDir(config.fixtureName); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: config.packageTemplateFile, + tsconfigTemplateFile: config.tsconfigTemplateFile, + tsVersion: TS5_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + if (config.withRuntime) { + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + } +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package TS5 consumer contract", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it( + `runs TS ESM modern lane (${TS5_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts5-esm-modern", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); + + it( + `runs TS CJS modern lane (${TS5_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts5-cjs-modern", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); + + it( + `runs TS CJS legacy lane (${TS5_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts5-cjs-legacy", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.legacy-commonjs.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); + + it( + `runs TS bundler compile-only lane (${TS5_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts5-bundler", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + withRuntime: false, + }); + }, + TS_TIMEOUT_MS, + ); +}); diff --git a/test/consumer/consumer-contract-ts6.spec.ts b/test/consumer/consumer-contract-ts6.spec.ts new file mode 100644 index 000000000..5c3b8e8be --- /dev/null +++ b/test/consumer/consumer-contract-ts6.spec.ts @@ -0,0 +1,96 @@ +import { afterAll, beforeAll, describe, it } from "vitest"; +import { + buildPackedTarball, + createTempDir, + installSdkTarball, + materializeTsFixture, + removeDir, + runCommand, +} from "./runner"; + +const repoRoot = process.cwd(); +const tempDirs: string[] = []; +let tarballPath = ""; +const TS6_RANGE = ">=6.0.0 <7"; +const TS_TIMEOUT_MS = 240_000; + +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +interface TsLaneConfig { + readonly fixtureName: string; + readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; + readonly tsconfigTemplateFile: + | "tsconfig.nodenext.json" + | "tsconfig.bundler.json"; + readonly withRuntime: boolean; +} + +async function runTsLane(config: TsLaneConfig): Promise { + const fixtureDir = await prepareFixtureDir(config.fixtureName); + await materializeTsFixture({ + repoRoot, + outDir: fixtureDir, + packageTemplateFile: config.packageTemplateFile, + tsconfigTemplateFile: config.tsconfigTemplateFile, + tsVersion: TS6_RANGE, + }); + + installSdkTarball(fixtureDir, tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + if (config.withRuntime) { + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + } +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + +describe("dual package TS6 consumer contract", () => { + beforeAll(async () => { + tarballPath = await buildPackedTarball(repoRoot); + }); + + it( + `runs TS ESM modern lane (${TS6_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts6-esm-modern", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); + + it( + `runs TS CJS modern lane (${TS6_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts6-cjs-modern", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); + + it( + `runs TS bundler compile-only lane (${TS6_RANGE})`, + async () => { + await runTsLane({ + fixtureName: "ts6-bundler", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + withRuntime: false, + }); + }, + TS_TIMEOUT_MS, + ); +}); diff --git a/test/consumer/fixtures/js-cjs/smoke-outbound.cjs b/test/consumer/fixtures/js-cjs/outbound-http-contract.cjs similarity index 100% rename from test/consumer/fixtures/js-cjs/smoke-outbound.cjs rename to test/consumer/fixtures/js-cjs/outbound-http-contract.cjs diff --git a/test/consumer/fixtures/js-cjs/smoke-resolution.cjs b/test/consumer/fixtures/js-cjs/resolution-load-check.cjs similarity index 100% rename from test/consumer/fixtures/js-cjs/smoke-resolution.cjs rename to test/consumer/fixtures/js-cjs/resolution-load-check.cjs diff --git a/test/consumer/fixtures/js-cjs/smoke-webhook.cjs b/test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs similarity index 100% rename from test/consumer/fixtures/js-cjs/smoke-webhook.cjs rename to test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs diff --git a/test/consumer/fixtures/js-esm/smoke-outbound.mjs b/test/consumer/fixtures/js-esm/outbound-http-contract.mjs similarity index 100% rename from test/consumer/fixtures/js-esm/smoke-outbound.mjs rename to test/consumer/fixtures/js-esm/outbound-http-contract.mjs diff --git a/test/consumer/fixtures/js-esm/smoke-resolution.mjs b/test/consumer/fixtures/js-esm/resolution-load-check.mjs similarity index 100% rename from test/consumer/fixtures/js-esm/smoke-resolution.mjs rename to test/consumer/fixtures/js-esm/resolution-load-check.mjs diff --git a/test/consumer/fixtures/js-esm/smoke-webhook.mjs b/test/consumer/fixtures/js-esm/webhook-signature-contract.mjs similarity index 100% rename from test/consumer/fixtures/js-esm/smoke-webhook.mjs rename to test/consumer/fixtures/js-esm/webhook-signature-contract.mjs diff --git a/test/consumer/harness/runner.ts b/test/consumer/runner/index.ts similarity index 93% rename from test/consumer/harness/runner.ts rename to test/consumer/runner/index.ts index 7f85b6218..cae4898e3 100644 --- a/test/consumer/harness/runner.ts +++ b/test/consumer/runner/index.ts @@ -97,14 +97,14 @@ export async function materializeTsFixture(params: { readonly tsconfigTemplateFile?: string; readonly tsVersion: string; }): Promise { - const blueprintRoot = path.join( + const templateRoot = path.join( params.repoRoot, - "test/consumer/blueprints/ts-modern", + "test/consumer/templates/ts-modern", ); - await copyDir(path.join(blueprintRoot, "files"), params.outDir); + await copyDir(path.join(templateRoot, "files"), params.outDir); const packageTemplate = await readFile( - path.join(blueprintRoot, params.packageTemplateFile), + path.join(templateRoot, params.packageTemplateFile), "utf8", ); const packageJson = packageTemplate.replace( @@ -119,7 +119,7 @@ export async function materializeTsFixture(params: { const tsconfig = await readFile( path.join( - blueprintRoot, + templateRoot, params.tsconfigTemplateFile ?? "tsconfig.nodenext.json", ), "utf8", diff --git a/test/consumer/smoke-js.spec.ts b/test/consumer/smoke-js.spec.ts deleted file mode 100644 index a17b16a90..000000000 --- a/test/consumer/smoke-js.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { afterAll, beforeAll, describe, it } from "vitest"; -import { - buildPackedTarball, - copyDir, - createTempDir, - installSdkTarball, - removeDir, - runCommand, -} from "./harness/runner"; - -const repoRoot = process.cwd(); -const tempDirs: string[] = []; -let tarballPath = ""; - -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - -afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); -}); - -describe("dual package JS consumer smoke", () => { - beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); - }); - - it("runs JS ESM fixture with resolution and behavioral smoke", async () => { - const fixtureDir = await prepareFixtureDir("js-esm"); - await copyDir(`${repoRoot}/test/consumer/fixtures/js-esm`, fixtureDir); - - installSdkTarball(fixtureDir, tarballPath); - - runCommand(fixtureDir, "node", ["./smoke-resolution.mjs"]); - runCommand(fixtureDir, "node", ["./smoke-outbound.mjs"]); - runCommand(fixtureDir, "node", ["./smoke-webhook.mjs"]); - }, 180_000); - - it("runs JS CJS fixture resolution smoke", async () => { - const fixtureDir = await prepareFixtureDir("js-cjs"); - await copyDir(`${repoRoot}/test/consumer/fixtures/js-cjs`, fixtureDir); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "node", ["./smoke-resolution.cjs"]); - runCommand(fixtureDir, "node", ["./smoke-outbound.cjs"]); - runCommand(fixtureDir, "node", ["./smoke-webhook.cjs"]); - }, 120_000); -}); diff --git a/test/consumer/smoke-ts5.spec.ts b/test/consumer/smoke-ts5.spec.ts deleted file mode 100644 index d08c4c23b..000000000 --- a/test/consumer/smoke-ts5.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { afterAll, beforeAll, describe, it } from "vitest"; -import { - buildPackedTarball, - createTempDir, - installSdkTarball, - materializeTsFixture, - removeDir, - runCommand, -} from "./harness/runner"; - -const repoRoot = process.cwd(); -const tempDirs: string[] = []; -let tarballPath = ""; -const TS5_RANGE = ">=5.5.4 <6"; - -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - -afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); -}); - -describe("dual package TS5 consumer smoke", () => { - beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); - }); - - it(`runs TS ESM modern lane (${TS5_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts5-esm-modern"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - tsVersion: TS5_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - }, 240_000); - - it(`runs TS CJS modern lane (${TS5_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts5-cjs-modern"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - tsVersion: TS5_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - }, 240_000); - - it(`runs TS CJS legacy lane (${TS5_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts5-cjs-legacy"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.legacy-commonjs.json", - tsVersion: TS5_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - }, 240_000); - - it(`runs TS bundler compile-only lane (${TS5_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts5-bundler"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.bundler.json", - tsVersion: TS5_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - }, 240_000); -}); diff --git a/test/consumer/smoke-ts6.spec.ts b/test/consumer/smoke-ts6.spec.ts deleted file mode 100644 index 2ddb57101..000000000 --- a/test/consumer/smoke-ts6.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { afterAll, beforeAll, describe, it } from "vitest"; -import { - buildPackedTarball, - createTempDir, - installSdkTarball, - materializeTsFixture, - removeDir, - runCommand, -} from "./harness/runner"; - -const repoRoot = process.cwd(); -const tempDirs: string[] = []; -let tarballPath = ""; -const TS6_RANGE = ">=6.0.0 <7"; - -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - -afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); -}); - -describe("dual package TS6 consumer smoke", () => { - beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); - }); - - it(`runs TS ESM modern lane (${TS6_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts6-esm-modern"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - tsVersion: TS6_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - }, 240_000); - - it(`runs TS CJS modern lane (${TS6_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts6-cjs-modern"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - tsVersion: TS6_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - }, 240_000); - - it(`runs TS bundler compile-only lane (${TS6_RANGE})`, async () => { - const fixtureDir = await prepareFixtureDir("ts6-bundler"); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.bundler.json", - tsVersion: TS6_RANGE, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - }, 240_000); -}); diff --git a/test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts b/test/consumer/templates/ts-modern/files/src/resolution-load-check.ts similarity index 100% rename from test/consumer/blueprints/ts-modern/files/src/smoke-resolution.ts rename to test/consumer/templates/ts-modern/files/src/resolution-load-check.ts diff --git a/test/consumer/blueprints/ts-modern/package.commonjs.json b/test/consumer/templates/ts-modern/package.commonjs.json similarity index 76% rename from test/consumer/blueprints/ts-modern/package.commonjs.json rename to test/consumer/templates/ts-modern/package.commonjs.json index 5b1c3b6c6..d75ac6294 100644 --- a/test/consumer/blueprints/ts-modern/package.commonjs.json +++ b/test/consumer/templates/ts-modern/package.commonjs.json @@ -7,6 +7,6 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "run:resolution": "node ./dist/smoke-resolution.js" + "run:resolution": "node ./dist/resolution-load-check.js" } } diff --git a/test/consumer/blueprints/ts-modern/package.module.json b/test/consumer/templates/ts-modern/package.module.json similarity index 76% rename from test/consumer/blueprints/ts-modern/package.module.json rename to test/consumer/templates/ts-modern/package.module.json index a6adda459..b934ce939 100644 --- a/test/consumer/blueprints/ts-modern/package.module.json +++ b/test/consumer/templates/ts-modern/package.module.json @@ -7,6 +7,6 @@ }, "scripts": { "build": "tsc -p ./tsconfig.json", - "run:resolution": "node ./dist/smoke-resolution.js" + "run:resolution": "node ./dist/resolution-load-check.js" } } diff --git a/test/consumer/blueprints/ts-modern/tsconfig.bundler.json b/test/consumer/templates/ts-modern/tsconfig.bundler.json similarity index 100% rename from test/consumer/blueprints/ts-modern/tsconfig.bundler.json rename to test/consumer/templates/ts-modern/tsconfig.bundler.json diff --git a/test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json b/test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json similarity index 100% rename from test/consumer/blueprints/ts-modern/tsconfig.legacy-commonjs.json rename to test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json diff --git a/test/consumer/blueprints/ts-modern/tsconfig.nodenext.json b/test/consumer/templates/ts-modern/tsconfig.nodenext.json similarity index 100% rename from test/consumer/blueprints/ts-modern/tsconfig.nodenext.json rename to test/consumer/templates/ts-modern/tsconfig.nodenext.json From 7a8da607de7efaedd472f6f73ccee5f9de21d1f5 Mon Sep 17 00:00:00 2001 From: Yuta Kasai Date: Sat, 9 May 2026 13:01:58 +0900 Subject: [PATCH 3/5] Add timeout for new process --- test/consumer/runner/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/consumer/runner/index.ts b/test/consumer/runner/index.ts index cae4898e3..cd71c3c81 100644 --- a/test/consumer/runner/index.ts +++ b/test/consumer/runner/index.ts @@ -9,6 +9,8 @@ export interface CmdResult { readonly stderr: string; } +const COMMAND_TIMEOUT_MS = 300_000; + export function runCommand( cwd: string, command: string, @@ -27,12 +29,28 @@ export function runCommand( cwd, env: { ...process.env, ...defaultEnv, ...env }, encoding: "utf8", + timeout: COMMAND_TIMEOUT_MS, }); + if (result.error) { + const joined = [ + `$ ${command} ${args.join(" ")}`, + `cwd: ${cwd}`, + `timeoutMs: ${COMMAND_TIMEOUT_MS}`, + result.stdout, + result.stderr, + result.error.message, + ] + .filter(Boolean) + .join("\n"); + throw new Error(joined); + } + if (result.status !== 0) { const joined = [ `$ ${command} ${args.join(" ")}`, `cwd: ${cwd}`, + `timeoutMs: ${COMMAND_TIMEOUT_MS}`, result.stdout, result.stderr, ] From cf118b72fcdb0bc826be596074f1b9b4dfc861f7 Mon Sep 17 00:00:00 2001 From: Yuta Kasai Date: Sat, 9 May 2026 13:42:58 +0900 Subject: [PATCH 4/5] polish 2 --- package.json | 2 +- test/consumer/consumer-contract-js.spec.ts | 3 +- .../consumer-contract-packaging.spec.ts | 18 +++- test/consumer/consumer-contract-ts5.spec.ts | 15 ++-- test/consumer/consumer-contract-ts6.spec.ts | 27 ++++-- .../js-cjs/outbound-http-contract.cjs | 14 ++- .../js-cjs/webhook-signature-contract.cjs | 6 +- .../js-esm/outbound-http-contract.mjs | 7 +- .../js-esm/webhook-signature-contract.mjs | 4 +- test/consumer/runner/index.ts | 86 +++++++++++++------ .../templates/ts-modern/tsconfig.bundler.json | 8 +- .../ts-modern/tsconfig.legacy-commonjs.json | 8 +- .../templates/ts-modern/tsconfig.node16.json | 17 ++++ .../ts-modern/tsconfig.nodenext.json | 8 +- 14 files changed, 157 insertions(+), 66 deletions(-) create mode 100644 test/consumer/templates/ts-modern/tsconfig.node16.json diff --git a/package.json b/package.json index c454a7028..65799dc1e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "scripts": { "test": "npm run format && npm run build && vitest run", "covtest": "vitest run --coverage", - "prettier": "prettier \"{lib,test}/**/*.ts\" \"scripts/**/*.{mjs,cjs,js}\" \"examples/**/*.{ts,js,mjs}\"", + "prettier": "prettier \"{lib,test,scripts,examples}/**/*.{ts,js,cjs,mjs}\"", "format": "npm run prettier -- --write", "format:check": "npm run prettier -- -l", "clean": "rm -rf dist/*", diff --git a/test/consumer/consumer-contract-js.spec.ts b/test/consumer/consumer-contract-js.spec.ts index db440f232..e196b2828 100644 --- a/test/consumer/consumer-contract-js.spec.ts +++ b/test/consumer/consumer-contract-js.spec.ts @@ -41,7 +41,8 @@ afterAll(async () => { describe("dual package JS consumer contract", () => { beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); + const packOutDir = await prepareFixtureDir("js-pack"); + tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); it( diff --git a/test/consumer/consumer-contract-packaging.spec.ts b/test/consumer/consumer-contract-packaging.spec.ts index 58c88ca1c..d64329c15 100644 --- a/test/consumer/consumer-contract-packaging.spec.ts +++ b/test/consumer/consumer-contract-packaging.spec.ts @@ -1,18 +1,32 @@ import assert from "node:assert/strict"; import { existsSync } from "node:fs"; -import { beforeAll, describe, it } from "vitest"; +import { afterAll, beforeAll, describe, it } from "vitest"; import { buildPackedTarball, + createTempDir, listTarEntries, readTarPackageJson, + removeDir, } from "./runner"; const repoRoot = process.cwd(); +const tempDirs: string[] = []; let tarballPath = ""; +async function prepareFixtureDir(name: string): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + +afterAll(async () => { + await Promise.all(tempDirs.map(dir => removeDir(dir))); +}); + describe("dual package packaging contract", () => { beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); + const packOutDir = await prepareFixtureDir("packaging-pack"); + tarballPath = await buildPackedTarball(repoRoot, packOutDir); assert.equal(existsSync(tarballPath), true); }); diff --git a/test/consumer/consumer-contract-ts5.spec.ts b/test/consumer/consumer-contract-ts5.spec.ts index 382a9cfcb..408162c93 100644 --- a/test/consumer/consumer-contract-ts5.spec.ts +++ b/test/consumer/consumer-contract-ts5.spec.ts @@ -11,7 +11,7 @@ import { const repoRoot = process.cwd(); const tempDirs: string[] = []; let tarballPath = ""; -const TS5_RANGE = ">=5.5.4 <6"; +const TS5_VERSION = "5.9.3"; const TS_TIMEOUT_MS = 240_000; async function prepareFixtureDir(name: string): Promise { @@ -37,7 +37,7 @@ async function runTsLane(config: TsLaneConfig): Promise { outDir: fixtureDir, packageTemplateFile: config.packageTemplateFile, tsconfigTemplateFile: config.tsconfigTemplateFile, - tsVersion: TS5_RANGE, + tsVersion: TS5_VERSION, }); installSdkTarball(fixtureDir, tarballPath); @@ -53,11 +53,12 @@ afterAll(async () => { describe("dual package TS5 consumer contract", () => { beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); + const packOutDir = await prepareFixtureDir("ts5-pack"); + tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); it( - `runs TS ESM modern lane (${TS5_RANGE})`, + `runs TS ESM modern lane (${TS5_VERSION})`, async () => { await runTsLane({ fixtureName: "ts5-esm-modern", @@ -70,7 +71,7 @@ describe("dual package TS5 consumer contract", () => { ); it( - `runs TS CJS modern lane (${TS5_RANGE})`, + `runs TS CJS modern lane (${TS5_VERSION})`, async () => { await runTsLane({ fixtureName: "ts5-cjs-modern", @@ -83,7 +84,7 @@ describe("dual package TS5 consumer contract", () => { ); it( - `runs TS CJS legacy lane (${TS5_RANGE})`, + `runs TS CJS legacy lane (${TS5_VERSION})`, async () => { await runTsLane({ fixtureName: "ts5-cjs-legacy", @@ -96,7 +97,7 @@ describe("dual package TS5 consumer contract", () => { ); it( - `runs TS bundler compile-only lane (${TS5_RANGE})`, + `runs TS bundler compile-only lane (${TS5_VERSION})`, async () => { await runTsLane({ fixtureName: "ts5-bundler", diff --git a/test/consumer/consumer-contract-ts6.spec.ts b/test/consumer/consumer-contract-ts6.spec.ts index 5c3b8e8be..90b1cb22c 100644 --- a/test/consumer/consumer-contract-ts6.spec.ts +++ b/test/consumer/consumer-contract-ts6.spec.ts @@ -11,7 +11,7 @@ import { const repoRoot = process.cwd(); const tempDirs: string[] = []; let tarballPath = ""; -const TS6_RANGE = ">=6.0.0 <7"; +const TS6_VERSION = "6.0.3"; const TS_TIMEOUT_MS = 240_000; async function prepareFixtureDir(name: string): Promise { @@ -25,6 +25,7 @@ interface TsLaneConfig { readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; readonly tsconfigTemplateFile: | "tsconfig.nodenext.json" + | "tsconfig.node16.json" | "tsconfig.bundler.json"; readonly withRuntime: boolean; } @@ -36,7 +37,7 @@ async function runTsLane(config: TsLaneConfig): Promise { outDir: fixtureDir, packageTemplateFile: config.packageTemplateFile, tsconfigTemplateFile: config.tsconfigTemplateFile, - tsVersion: TS6_RANGE, + tsVersion: TS6_VERSION, }); installSdkTarball(fixtureDir, tarballPath); @@ -52,11 +53,12 @@ afterAll(async () => { describe("dual package TS6 consumer contract", () => { beforeAll(async () => { - tarballPath = await buildPackedTarball(repoRoot); + const packOutDir = await prepareFixtureDir("ts6-pack"); + tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); it( - `runs TS ESM modern lane (${TS6_RANGE})`, + `runs TS ESM modern lane (${TS6_VERSION})`, async () => { await runTsLane({ fixtureName: "ts6-esm-modern", @@ -69,7 +71,7 @@ describe("dual package TS6 consumer contract", () => { ); it( - `runs TS CJS modern lane (${TS6_RANGE})`, + `runs TS CJS modern lane (${TS6_VERSION})`, async () => { await runTsLane({ fixtureName: "ts6-cjs-modern", @@ -82,7 +84,7 @@ describe("dual package TS6 consumer contract", () => { ); it( - `runs TS bundler compile-only lane (${TS6_RANGE})`, + `runs TS bundler compile-only lane (${TS6_VERSION})`, async () => { await runTsLane({ fixtureName: "ts6-bundler", @@ -93,4 +95,17 @@ describe("dual package TS6 consumer contract", () => { }, TS_TIMEOUT_MS, ); + + it( + `runs TS CJS node16 lane (${TS6_VERSION})`, + async () => { + await runTsLane({ + fixtureName: "ts6-cjs-node16", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.node16.json", + withRuntime: true, + }); + }, + TS_TIMEOUT_MS, + ); }); diff --git a/test/consumer/fixtures/js-cjs/outbound-http-contract.cjs b/test/consumer/fixtures/js-cjs/outbound-http-contract.cjs index d4a049173..972d195d7 100644 --- a/test/consumer/fixtures/js-cjs/outbound-http-contract.cjs +++ b/test/consumer/fixtures/js-cjs/outbound-http-contract.cjs @@ -70,9 +70,8 @@ async function main() { const botInfo = await client.getBotInfo(); assert.equal(botInfo.displayName, "Test Bot"); - const transcoding = await client.getMessageContentTranscodingByMessageId( - "message-1", - ); + const transcoding = + await client.getMessageContentTranscodingByMessageId("message-1"); assert.equal(transcoding.status, "succeeded"); await client.attachModule( @@ -84,7 +83,7 @@ async function main() { assert.equal(requests.length, 3); assert.deepEqual( - requests.map((request) => ({ method: request.method, url: request.url })), + requests.map(request => ({ method: request.method, url: request.url })), [ { method: "GET", url: "/v2/bot/info" }, { @@ -102,16 +101,13 @@ async function main() { const formBody = requests[2]?.body ?? ""; assert.match(formBody, /grant_type=authorization_code/); assert.match(formBody, /code=code-1/); - assert.match( - formBody, - /redirect_uri=https%3A%2F%2Fexample.com%2Fcallback/, - ); + assert.match(formBody, /redirect_uri=https%3A%2F%2Fexample.com%2Fcallback/); } finally { server.close(); } } -main().catch((error) => { +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs b/test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs index afdbbe8f0..d83031374 100644 --- a/test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs +++ b/test/consumer/fixtures/js-cjs/webhook-signature-contract.cjs @@ -6,14 +6,14 @@ const { middleware } = require("@line/bot-sdk"); const channelSecret = "channel-secret"; -const makeSignature = (body) => { +const makeSignature = body => { return createHmac("sha256", channelSecret).update(body).digest("base64"); }; const mw = middleware({ channelSecret }); const server = createServer((req, res) => { - mw(req, res, (err) => { + mw(req, res, err => { if (err) { res.statusCode = 401; res.end("invalid"); @@ -67,7 +67,7 @@ async function main() { } } -main().catch((error) => { +main().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/test/consumer/fixtures/js-esm/outbound-http-contract.mjs b/test/consumer/fixtures/js-esm/outbound-http-contract.mjs index 2d907f164..462aeaca9 100644 --- a/test/consumer/fixtures/js-esm/outbound-http-contract.mjs +++ b/test/consumer/fixtures/js-esm/outbound-http-contract.mjs @@ -69,9 +69,8 @@ try { const botInfo = await client.getBotInfo(); assert.equal(botInfo.displayName, "Test Bot"); - const transcoding = await client.getMessageContentTranscodingByMessageId( - "message-1", - ); + const transcoding = + await client.getMessageContentTranscodingByMessageId("message-1"); assert.equal(transcoding.status, "succeeded"); await client.attachModule( @@ -83,7 +82,7 @@ try { assert.equal(requests.length, 3); assert.deepEqual( - requests.map((request) => ({ method: request.method, url: request.url })), + requests.map(request => ({ method: request.method, url: request.url })), [ { method: "GET", url: "/v2/bot/info" }, { method: "GET", url: "/v2/bot/message/message-1/content/transcoding" }, diff --git a/test/consumer/fixtures/js-esm/webhook-signature-contract.mjs b/test/consumer/fixtures/js-esm/webhook-signature-contract.mjs index fb901e875..14c54689a 100644 --- a/test/consumer/fixtures/js-esm/webhook-signature-contract.mjs +++ b/test/consumer/fixtures/js-esm/webhook-signature-contract.mjs @@ -6,14 +6,14 @@ import { middleware } from "@line/bot-sdk"; const channelSecret = "channel-secret"; -const makeSignature = (body) => { +const makeSignature = body => { return createHmac("sha256", channelSecret).update(body).digest("base64"); }; const mw = middleware({ channelSecret }); const server = createServer((req, res) => { - mw(req, res, (err) => { + mw(req, res, err => { if (err) { res.statusCode = 401; res.end("invalid"); diff --git a/test/consumer/runner/index.ts b/test/consumer/runner/index.ts index cd71c3c81..6070ace44 100644 --- a/test/consumer/runner/index.ts +++ b/test/consumer/runner/index.ts @@ -9,34 +9,55 @@ export interface CmdResult { readonly stderr: string; } -const COMMAND_TIMEOUT_MS = 300_000; +export interface PackagedPackageJson { + readonly main: string; + readonly types: string; + readonly exports: { + readonly ".": { + readonly import: { + readonly types: string; + readonly default: string; + }; + readonly require: { + readonly types: string; + readonly default: string; + }; + }; + }; +} + +const COMMAND_TIMEOUT_MS = 45_000; +const NPM_TIMEOUT_MS = 60_000; export function runCommand( cwd: string, command: string, args: string[], env: Record = {}, + timeoutMs: number = COMMAND_TIMEOUT_MS, ): CmdResult { - const defaultEnv = - command === "npm" && env.NPM_CONFIG_CACHE == null - ? { - NPM_CONFIG_CACHE: path.join(os.tmpdir(), "bot-sdk-npm-cache"), - NPM_CONFIG_REGISTRY: "https://registry.npmjs.org", - } - : {}; + const defaultEnv: Record = {}; + if (command === "npm") { + if (env.NPM_CONFIG_CACHE == null) { + defaultEnv.NPM_CONFIG_CACHE = path.join(os.tmpdir(), "bot-sdk-npm-cache"); + } + if (env.NPM_CONFIG_REGISTRY == null) { + defaultEnv.NPM_CONFIG_REGISTRY = "https://registry.npmjs.org"; + } + } const result = spawnSync(command, args, { cwd, env: { ...process.env, ...defaultEnv, ...env }, encoding: "utf8", - timeout: COMMAND_TIMEOUT_MS, + timeout: timeoutMs, }); if (result.error) { const joined = [ `$ ${command} ${args.join(" ")}`, `cwd: ${cwd}`, - `timeoutMs: ${COMMAND_TIMEOUT_MS}`, + `timeoutMs: ${timeoutMs}`, result.stdout, result.stderr, result.error.message, @@ -50,7 +71,7 @@ export function runCommand( const joined = [ `$ ${command} ${args.join(" ")}`, `cwd: ${cwd}`, - `timeoutMs: ${COMMAND_TIMEOUT_MS}`, + `timeoutMs: ${timeoutMs}`, result.stdout, result.stderr, ] @@ -77,24 +98,33 @@ export async function copyDir(src: string, dst: string): Promise { await cp(src, dst, { recursive: true }); } -export async function buildPackedTarball(repoRoot: string): Promise { - const { stdout } = runCommand(repoRoot, "npm", ["pack", "--json"]); +export async function buildPackedTarball( + repoRoot: string, + outDir: string, +): Promise { + const { stdout } = runCommand( + repoRoot, + "npm", + ["pack", "--json", "--pack-destination", outDir], + {}, + NPM_TIMEOUT_MS, + ); const parsed = JSON.parse(stdout) as Array<{ filename: string }>; const filename = parsed[0]?.filename; assert.ok(filename, "npm pack --json did not return filename"); - return path.join(repoRoot, filename); + return path.join(outDir, filename); } export async function readTarPackageJson( repoRoot: string, tarballPath: string, -): Promise { +): Promise { const { stdout } = runCommand(repoRoot, "tar", [ "-xOf", tarballPath, "package/package.json", ]); - return JSON.parse(stdout); + return JSON.parse(stdout) as PackagedPackageJson; } export function listTarEntries( @@ -149,13 +179,19 @@ export function installSdkTarball( projectDir: string, tarballPath: string, ): void { - runCommand(projectDir, "npm", [ - "install", - "--no-audit", - "--no-fund", - "--ignore-scripts", - "--prefer-offline", - "--no-save", - `file:${tarballPath}`, - ]); + runCommand( + projectDir, + "npm", + [ + "install", + "--no-audit", + "--no-fund", + "--ignore-scripts", + "--prefer-offline", + "--no-save", + `file:${tarballPath}`, + ], + {}, + NPM_TIMEOUT_MS, + ); } diff --git a/test/consumer/templates/ts-modern/tsconfig.bundler.json b/test/consumer/templates/ts-modern/tsconfig.bundler.json index dbb1d841c..f07f73f54 100644 --- a/test/consumer/templates/ts-modern/tsconfig.bundler.json +++ b/test/consumer/templates/ts-modern/tsconfig.bundler.json @@ -5,9 +5,13 @@ "moduleResolution": "Bundler", "strict": true, "skipLibCheck": false, - "types": ["node"], + "types": [ + "node" + ], "rootDir": "src", "outDir": "dist" }, - "include": ["src/**/*.ts"] + "include": [ + "src/**/*.ts" + ] } diff --git a/test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json b/test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json index cb76c687e..0e77706d8 100644 --- a/test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json +++ b/test/consumer/templates/ts-modern/tsconfig.legacy-commonjs.json @@ -5,9 +5,13 @@ "moduleResolution": "Node", "strict": true, "skipLibCheck": false, - "types": ["node"], + "types": [ + "node" + ], "rootDir": "src", "outDir": "dist" }, - "include": ["src/**/*.ts"] + "include": [ + "src/**/*.ts" + ] } diff --git a/test/consumer/templates/ts-modern/tsconfig.node16.json b/test/consumer/templates/ts-modern/tsconfig.node16.json new file mode 100644 index 000000000..8e4d2825a --- /dev/null +++ b/test/consumer/templates/ts-modern/tsconfig.node16.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "skipLibCheck": false, + "types": [ + "node" + ], + "rootDir": "src", + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/test/consumer/templates/ts-modern/tsconfig.nodenext.json b/test/consumer/templates/ts-modern/tsconfig.nodenext.json index 3b39615e5..075995082 100644 --- a/test/consumer/templates/ts-modern/tsconfig.nodenext.json +++ b/test/consumer/templates/ts-modern/tsconfig.nodenext.json @@ -5,9 +5,13 @@ "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": false, - "types": ["node"], + "types": [ + "node" + ], "rootDir": "src", "outDir": "dist" }, - "include": ["src/**/*.ts"] + "include": [ + "src/**/*.ts" + ] } From 323676e11d9963d555b7ce4282ff63fa114b608a Mon Sep 17 00:00:00 2001 From: Yuta Kasai Date: Tue, 19 May 2026 15:52:59 +0900 Subject: [PATCH 5/5] setup common of common --- test/consumer/consumer-contract-js.spec.ts | 14 +-- .../consumer-contract-packaging.spec.ts | 12 +- test/consumer/consumer-contract-ts5.spec.ts | 107 +++++++----------- test/consumer/consumer-contract-ts6.spec.ts | 107 +++++++----------- test/consumer/runner/index.ts | 9 ++ test/consumer/runner/ts-lane.ts | 43 +++++++ 6 files changed, 139 insertions(+), 153 deletions(-) create mode 100644 test/consumer/runner/ts-lane.ts diff --git a/test/consumer/consumer-contract-js.spec.ts b/test/consumer/consumer-contract-js.spec.ts index e196b2828..51f4c304d 100644 --- a/test/consumer/consumer-contract-js.spec.ts +++ b/test/consumer/consumer-contract-js.spec.ts @@ -2,8 +2,8 @@ import { afterAll, beforeAll, describe, it } from "vitest"; import { buildPackedTarball, copyDir, - createTempDir, installSdkTarball, + prepareFixtureDir, removeDir, runCommand, } from "./runner"; @@ -13,17 +13,11 @@ const tempDirs: string[] = []; let tarballPath = ""; const JS_TIMEOUT_MS = 180_000; -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - async function runJsFixture( fixtureName: "js-esm" | "js-cjs", scripts: readonly string[], ): Promise { - const fixtureDir = await prepareFixtureDir(fixtureName); + const fixtureDir = await prepareFixtureDir(tempDirs, fixtureName); await copyDir( `${repoRoot}/test/consumer/fixtures/${fixtureName}`, fixtureDir, @@ -36,12 +30,12 @@ async function runJsFixture( } afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); + await Promise.allSettled(tempDirs.map(dir => removeDir(dir))); }); describe("dual package JS consumer contract", () => { beforeAll(async () => { - const packOutDir = await prepareFixtureDir("js-pack"); + const packOutDir = await prepareFixtureDir(tempDirs, "js-pack"); tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); diff --git a/test/consumer/consumer-contract-packaging.spec.ts b/test/consumer/consumer-contract-packaging.spec.ts index d64329c15..6d40b545d 100644 --- a/test/consumer/consumer-contract-packaging.spec.ts +++ b/test/consumer/consumer-contract-packaging.spec.ts @@ -3,8 +3,8 @@ import { existsSync } from "node:fs"; import { afterAll, beforeAll, describe, it } from "vitest"; import { buildPackedTarball, - createTempDir, listTarEntries, + prepareFixtureDir, readTarPackageJson, removeDir, } from "./runner"; @@ -13,19 +13,13 @@ const repoRoot = process.cwd(); const tempDirs: string[] = []; let tarballPath = ""; -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); + await Promise.allSettled(tempDirs.map(dir => removeDir(dir))); }); describe("dual package packaging contract", () => { beforeAll(async () => { - const packOutDir = await prepareFixtureDir("packaging-pack"); + const packOutDir = await prepareFixtureDir(tempDirs, "packaging-pack"); tarballPath = await buildPackedTarball(repoRoot, packOutDir); assert.equal(existsSync(tarballPath), true); }); diff --git a/test/consumer/consumer-contract-ts5.spec.ts b/test/consumer/consumer-contract-ts5.spec.ts index 408162c93..47bdaa78d 100644 --- a/test/consumer/consumer-contract-ts5.spec.ts +++ b/test/consumer/consumer-contract-ts5.spec.ts @@ -1,12 +1,6 @@ import { afterAll, beforeAll, describe, it } from "vitest"; -import { - buildPackedTarball, - createTempDir, - installSdkTarball, - materializeTsFixture, - removeDir, - runCommand, -} from "./runner"; +import { buildPackedTarball, prepareFixtureDir, removeDir } from "./runner"; +import { runTsLane } from "./runner/ts-lane"; const repoRoot = process.cwd(); const tempDirs: string[] = []; @@ -14,58 +8,28 @@ let tarballPath = ""; const TS5_VERSION = "5.9.3"; const TS_TIMEOUT_MS = 240_000; -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - -interface TsLaneConfig { - readonly fixtureName: string; - readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; - readonly tsconfigTemplateFile: - | "tsconfig.nodenext.json" - | "tsconfig.legacy-commonjs.json" - | "tsconfig.bundler.json"; - readonly withRuntime: boolean; -} - -async function runTsLane(config: TsLaneConfig): Promise { - const fixtureDir = await prepareFixtureDir(config.fixtureName); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: config.packageTemplateFile, - tsconfigTemplateFile: config.tsconfigTemplateFile, - tsVersion: TS5_VERSION, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - if (config.withRuntime) { - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - } -} - afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); + await Promise.allSettled(tempDirs.map(dir => removeDir(dir))); }); describe("dual package TS5 consumer contract", () => { beforeAll(async () => { - const packOutDir = await prepareFixtureDir("ts5-pack"); + const packOutDir = await prepareFixtureDir(tempDirs, "ts5-pack"); tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); it( `runs TS ESM modern lane (${TS5_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts5-esm-modern", - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs }, + { + fixtureName: "ts5-esm-modern", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); @@ -73,12 +37,15 @@ describe("dual package TS5 consumer contract", () => { it( `runs TS CJS modern lane (${TS5_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts5-cjs-modern", - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs }, + { + fixtureName: "ts5-cjs-modern", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); @@ -86,12 +53,15 @@ describe("dual package TS5 consumer contract", () => { it( `runs TS CJS legacy lane (${TS5_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts5-cjs-legacy", - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.legacy-commonjs.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs }, + { + fixtureName: "ts5-cjs-legacy", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.legacy-commonjs.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); @@ -99,12 +69,15 @@ describe("dual package TS5 consumer contract", () => { it( `runs TS bundler compile-only lane (${TS5_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts5-bundler", - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.bundler.json", - withRuntime: false, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS5_VERSION, tempDirs }, + { + fixtureName: "ts5-bundler", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + withRuntime: false, + }, + ); }, TS_TIMEOUT_MS, ); diff --git a/test/consumer/consumer-contract-ts6.spec.ts b/test/consumer/consumer-contract-ts6.spec.ts index 90b1cb22c..4828f0b45 100644 --- a/test/consumer/consumer-contract-ts6.spec.ts +++ b/test/consumer/consumer-contract-ts6.spec.ts @@ -1,12 +1,6 @@ import { afterAll, beforeAll, describe, it } from "vitest"; -import { - buildPackedTarball, - createTempDir, - installSdkTarball, - materializeTsFixture, - removeDir, - runCommand, -} from "./runner"; +import { buildPackedTarball, prepareFixtureDir, removeDir } from "./runner"; +import { runTsLane } from "./runner/ts-lane"; const repoRoot = process.cwd(); const tempDirs: string[] = []; @@ -14,58 +8,28 @@ let tarballPath = ""; const TS6_VERSION = "6.0.3"; const TS_TIMEOUT_MS = 240_000; -async function prepareFixtureDir(name: string): Promise { - const dir = await createTempDir(`bot-sdk-consumer-${name}-`); - tempDirs.push(dir); - return dir; -} - -interface TsLaneConfig { - readonly fixtureName: string; - readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; - readonly tsconfigTemplateFile: - | "tsconfig.nodenext.json" - | "tsconfig.node16.json" - | "tsconfig.bundler.json"; - readonly withRuntime: boolean; -} - -async function runTsLane(config: TsLaneConfig): Promise { - const fixtureDir = await prepareFixtureDir(config.fixtureName); - await materializeTsFixture({ - repoRoot, - outDir: fixtureDir, - packageTemplateFile: config.packageTemplateFile, - tsconfigTemplateFile: config.tsconfigTemplateFile, - tsVersion: TS6_VERSION, - }); - - installSdkTarball(fixtureDir, tarballPath); - runCommand(fixtureDir, "npm", ["run", "build"]); - if (config.withRuntime) { - runCommand(fixtureDir, "npm", ["run", "run:resolution"]); - } -} - afterAll(async () => { - await Promise.all(tempDirs.map(dir => removeDir(dir))); + await Promise.allSettled(tempDirs.map(dir => removeDir(dir))); }); describe("dual package TS6 consumer contract", () => { beforeAll(async () => { - const packOutDir = await prepareFixtureDir("ts6-pack"); + const packOutDir = await prepareFixtureDir(tempDirs, "ts6-pack"); tarballPath = await buildPackedTarball(repoRoot, packOutDir); }); it( `runs TS ESM modern lane (${TS6_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts6-esm-modern", - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs }, + { + fixtureName: "ts6-esm-modern", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); @@ -73,12 +37,15 @@ describe("dual package TS6 consumer contract", () => { it( `runs TS CJS modern lane (${TS6_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts6-cjs-modern", - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.nodenext.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs }, + { + fixtureName: "ts6-cjs-modern", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.nodenext.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); @@ -86,12 +53,15 @@ describe("dual package TS6 consumer contract", () => { it( `runs TS bundler compile-only lane (${TS6_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts6-bundler", - packageTemplateFile: "package.module.json", - tsconfigTemplateFile: "tsconfig.bundler.json", - withRuntime: false, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs }, + { + fixtureName: "ts6-bundler", + packageTemplateFile: "package.module.json", + tsconfigTemplateFile: "tsconfig.bundler.json", + withRuntime: false, + }, + ); }, TS_TIMEOUT_MS, ); @@ -99,12 +69,15 @@ describe("dual package TS6 consumer contract", () => { it( `runs TS CJS node16 lane (${TS6_VERSION})`, async () => { - await runTsLane({ - fixtureName: "ts6-cjs-node16", - packageTemplateFile: "package.commonjs.json", - tsconfigTemplateFile: "tsconfig.node16.json", - withRuntime: true, - }); + await runTsLane( + { repoRoot, tarballPath, tsVersion: TS6_VERSION, tempDirs }, + { + fixtureName: "ts6-cjs-node16", + packageTemplateFile: "package.commonjs.json", + tsconfigTemplateFile: "tsconfig.node16.json", + withRuntime: true, + }, + ); }, TS_TIMEOUT_MS, ); diff --git a/test/consumer/runner/index.ts b/test/consumer/runner/index.ts index 6070ace44..8a8b8a94a 100644 --- a/test/consumer/runner/index.ts +++ b/test/consumer/runner/index.ts @@ -94,6 +94,15 @@ export async function removeDir(dirPath: string): Promise { await rm(dirPath, { recursive: true, force: true }); } +export async function prepareFixtureDir( + tempDirs: string[], + name: string, +): Promise { + const dir = await createTempDir(`bot-sdk-consumer-${name}-`); + tempDirs.push(dir); + return dir; +} + export async function copyDir(src: string, dst: string): Promise { await cp(src, dst, { recursive: true }); } diff --git a/test/consumer/runner/ts-lane.ts b/test/consumer/runner/ts-lane.ts new file mode 100644 index 000000000..e8e35d0eb --- /dev/null +++ b/test/consumer/runner/ts-lane.ts @@ -0,0 +1,43 @@ +import { + installSdkTarball, + materializeTsFixture, + prepareFixtureDir, + runCommand, +} from "./index"; + +export interface TsLaneConfig { + readonly fixtureName: string; + readonly packageTemplateFile: "package.module.json" | "package.commonjs.json"; + readonly tsconfigTemplateFile: string; + readonly withRuntime: boolean; +} + +export interface TsLaneRunContext { + readonly repoRoot: string; + readonly tarballPath: string; + readonly tsVersion: string; + readonly tempDirs: string[]; +} + +export async function runTsLane( + context: TsLaneRunContext, + config: TsLaneConfig, +): Promise { + const fixtureDir = await prepareFixtureDir( + context.tempDirs, + config.fixtureName, + ); + await materializeTsFixture({ + repoRoot: context.repoRoot, + outDir: fixtureDir, + packageTemplateFile: config.packageTemplateFile, + tsconfigTemplateFile: config.tsconfigTemplateFile, + tsVersion: context.tsVersion, + }); + + installSdkTarball(fixtureDir, context.tarballPath); + runCommand(fixtureDir, "npm", ["run", "build"]); + if (config.withRuntime) { + runCommand(fixtureDir, "npm", ["run", "run:resolution"]); + } +}