diff --git a/implementations/cli/smoke/legacy-packaged-manager.smoke.ts b/implementations/cli/smoke/legacy-packaged-manager.smoke.ts index 16f91daa0..cccf2ecaa 100644 --- a/implementations/cli/smoke/legacy-packaged-manager.smoke.ts +++ b/implementations/cli/smoke/legacy-packaged-manager.smoke.ts @@ -4,7 +4,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSyn import { createServer, type AddressInfo } from "node:net"; import { tmpdir } from "node:os"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; const originalHome = process.env.HOME ?? homedir(); const originalXdg = process.env.XDG_CONFIG_HOME ?? join(originalHome, ".config"); @@ -58,13 +58,69 @@ try { assert.equal(spawnSync("sh", ["-c", "command -v cotal"], { env: cleanEnv, encoding: "utf8" }).stdout.trim(), fixtureCotal, "the fixture PATH masks the operator cotal binary"); assert.equal(run(npm, ["root"], current).stdout.trim(), join(realpathSync(current), "node_modules"), "current package install resolves into the isolated prefix before installation"); assert.equal(run(npm, ["root"], old).stdout.trim(), join(realpathSync(old), "node_modules"), "old package install resolves into the isolated prefix before installation"); - for (const dir of ["bin", "packages/core", "packages/workspace", "implementations/cli", "implementations/manager", "implementations/delivery", "implementations/auth", "extensions/connector-core"]) { + // The packed set is DERIVED, never hand-listed. A hand-listed set silently omits the next + // workspace package anyone adds to a dependency here, and the omission is invisible while the + // missing package happens to be published at the current version: npm fetches the registry copy + // and the install succeeds, so a cell named "installed current packaged closure" passes while one + // member came from npm rather than from this build. That is how `@cotal-ai/runtime` sat outside + // the set unnoticed, and it only surfaced on a release PR, where the version being packed is by + // definition not yet published. + const workspacePackages = new Map(); + for (const dir of readdirSync(repo, { withFileTypes: true }).filter((e) => e.isDirectory()).flatMap((e) => + ["bin"].includes(e.name) ? [e.name] : readdirSync(join(repo, e.name), { withFileTypes: true }) + .filter((c) => c.isDirectory()).map((c) => join(e.name, c.name)))) { + try { + const meta = JSON.parse(readFileSync(join(repo, dir, "package.json"), "utf8")) as { name?: string; private?: boolean }; + if (meta.name && !meta.private) workspacePackages.set(meta.name, dir); + } catch { /* not a package */ } + } + // Start from the entry point and take the transitive closure of its workspace: deps. + const needed = new Set(); + const queue = ["cotal-ai"]; + while (queue.length) { + const name = queue.shift()!; + const dir = workspacePackages.get(name); + if (dir === undefined || needed.has(name)) continue; + needed.add(name); + const meta = JSON.parse(readFileSync(join(repo, dir, "package.json"), "utf8")) as { dependencies?: Record }; + for (const [dep, range] of Object.entries(meta.dependencies ?? {})) if (range.startsWith("workspace:")) queue.push(dep); + } + for (const name of needed) { + const dir = workspacePackages.get(name)!; const packed = run(pnpm, ["-C", join(repo, dir), "pack", "--pack-destination", packs], repo); assert.equal(packed.status, 0, `packed ${dir}: ${packed.stdout}\n${packed.stderr}`); } const tarballs = readdirSync(packs).filter((name) => name.endsWith(".tgz")).map((name) => join(packs, name)); + assert.equal(tarballs.length, needed.size, `packed ${tarballs.length} tarball(s) for ${needed.size} closure member(s): ${[...needed].join(", ")}`); writeFileSync(join(current, "package.json"), JSON.stringify({ name: "current-fixture", private: true })); - assert.equal(run(npm, ["install", "--ignore-scripts", "--no-audit", "--no-fund", ...tarballs], current).status, 0, "installed current packaged closure"); + const install = run(npm, ["install", "--ignore-scripts", "--no-audit", "--no-fund", ...tarballs], current); + assert.equal(install.status, 0, `installed current packaged closure: ${install.stdout}\n${install.stderr}`); + // The guard is PROVENANCE, and deliberately not `--offline`. Forbidding the registry outright was + // tried and is wrong here: this fixture installs under an isolated HOME whose npm cache starts + // empty, so `--offline` fails on legitimate third-party dependencies -- `@cotal-ai/lang` needs + // `acorn` -- exactly as readily as on a workspace package that leaked to the registry. A complete + // and correct closure still ENOTCACHEDs, so the guard would red every run including the release + // it exists to unblock. It cannot tell the defect from the intended behaviour. + // + // The narrower property is the one worth asserting: any WORKSPACE package that ends up installed + // must have come from a tarball this run packed. Third-party packages resolve from npm, which is + // correct and stays silent. Membership is keyed on the workspace set rather than on an + // `@cotal-ai/` name prefix, because the entry point `cotal-ai` carries no scope and a prefix test + // would exempt the one package the closure is rooted at. + const packedTarballs = new Set(tarballs.map((path) => basename(path))); + const lock = JSON.parse(readFileSync(join(current, "node_modules", ".package-lock.json"), "utf8")) as { packages?: Record }; + let checked = 0; + for (const [path, entry] of Object.entries(lock.packages ?? {})) { + const name = path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length); + if (!workspacePackages.has(name)) continue; + checked += 1; + // A missing `resolved` fails. An unrecorded source is an unanswered question, not a clean bill. + assert.ok(entry.resolved?.startsWith("file:") && packedTarballs.has(basename(entry.resolved)), + `${name} resolved from ${entry.resolved ?? "an unrecorded source"} rather than from a tarball packed by this run: it came from the registry, so the packed closure is incomplete`); + } + // Without this the guard goes vacuous the day npm moves the hidden lockfile or reshapes its keys: + // nothing would match, zero packages would be checked, and the loop above would pass in silence. + assert.equal(checked, needed.size, `provenance checked ${checked} workspace package(s) but the closure has ${needed.size} -- the lockfile is not being read as expected`); writeFileSync(join(old, "package.json"), JSON.stringify({ name: "old-fixture", private: true })); assert.equal(run(npm, ["install", "--ignore-scripts", "--no-audit", "--no-fund", "cotal-ai@0.42.0"], old).status, 0, "installed published old package"); const oldRuntime = readFileSync(join(old, "node_modules", "@cotal-ai", "core", "dist", "runtime.js"), "utf8");