mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
b232114422
Audits every comment in `src/` and `scripts/` against the code it
describes.
**462 files, 57 commits, net −8,194 lines.** Comment text only — the
whole branch is code-identical to `main`.
## Method
One subagent per file, strictly sequential. Scope was `src/` +
`scripts/` (459 files); three more files were added at the end because
they carried dead references of the same kind — two test headers citing
design docs that do not exist, and `publish-npm.yml` citing a retired
workflow. Each agent verified every comment — line, block, JSDoc, file
header, trailing — against the surrounding code and the rest of the
repo, following identifiers, paths, tool ids, config keys, env vars and
issue links to see whether they still exist and still behave as
described. Rules:
- **A false or misleading comment is deleted, not reworded.** If a claim
could not be confirmed by reading the source, it went. That is why the
deletion count is so much larger than the rewrite count.
- Survivors are cut to the shortest form carrying something the code
does not already say. Restatement, preamble, hedging, changelog prose
and ASCII banners are gone; the non-obvious *why* stays.
- Preserved byte-identical: license headers, pragmas and directives
(`@ts-*`, `eslint-disable`, shebangs, `/// <reference>`), JSDoc tag
tokens, everything inside a string or template literal, and the sole
comment inside an otherwise empty block (ESLint `no-empty` counts a
comment-bearing block as non-empty).
## Verification
Every file passed two independent gates before being recorded as done:
1. `comments-only` — the required check.
2. A second comment-stripping comparator with a proper mode stack,
written for this pass because `comments-only`'s flat scanner desyncs on
nested template literals and quote-bearing regex literals and then
reports comment lines as code changes. Two files hit that false FAIL
(`utils/android-profiler/pipeline/index.ts`,
`scripts/extract-tools.mjs`); in both the "changed code" it printed was
literally `//` lines, and the second checker confirmed the code was
byte-identical.
After the last file, all 462 changed files were re-checked against
`main` with the same comparator, rather than trusting any agent's
self-report. **459 code-identical; 2 are non-code (`.svg`, `.md`); 1
intentional.**
The intentional one is `packages/argent/scripts/bundle-tools.cjs`: the
changed template literal *is* the comment header of the file it
generates, `packages/native-devtools-android/src/bundled-meta.ts`.
Fixing only the generated file would have been reverted by the next
build, so the generator changed too — and it has been verified to
reproduce the committed generated file byte-for-byte.
## Representative false claims removed
Not wording nits — statements a reader would have acted on:
- **Reversed directions.** `proxyStart`'s JSDoc had the tunnel backwards
(it is a reverse tunnel: the host binds first and the simulator dials
in). A `paste()` doc had the pasteboard copy direction reversed.
- **Contradicted by the code below it.** A timeout budget multiplied by
three where the probes run concurrently — the same comment said so six
lines later. A "warn once" that warns on every call. A "binary search"
that is a linear scan.
- **Named things that do not exist.** A `vega-fast-cli` binary, a
`finish-recording.ts`, a `publish-next.yml` workflow, two
`profiler-react19-*.md` design docs, a `DebuggerTarget.ts`, a commit
hash git does not know, two tool ids, an `ensureEnv` cycle.
- **Wrong by construction.** "Welford accumulators" across four files
where the code keeps naive `n`/`sum`/`sumSq`; `sum`/`sumSq` documented
over `actualDuration` when reduce sums `selfDuration`; a strict-mode
halving written `n/2` where the code ceils; field docs listing enum
values the producers never emit.
- **Guarantees the code does not make.** A validation matrix claiming to
cover "EVERY tool" that skips flagless ones; a Pareto cutoff that
`slice(0, 20)` makes inert; an idempotence claim where the real rule is
at-or-ahead; a capability note describing a clean 400 the shape-based
device resolver can never produce.
- **Unverifiable assertions** about prebuilt binaries, external CLIs and
the cloud SDK — deleted rather than kept as folklore, since nothing in
the repo can confirm them.
- **Stale numbers**: invented Android tool versions, hard-coded tool
counts and description lengths that had drifted.
## Review
A Fable agent reviewed both halves adversarially for over-deletion,
misread code, `no-empty` hazards and byte-identity violations.
Second-half verdict: **SHIP**, with two one-line restores, both applied
in the final commit — the `npm view ""` rationale behind a blank-token
guard, and the note that `argent-mcp` keeps a copy of
`SECRET_PLACEHOLDER_MARKER` it cannot import.
## Code issues surfaced but deliberately not fixed
This pass changes comments only. Eight genuine findings are logged for a
follow-up:
1. `telemetry/src/consent.ts` — a non-ENOENT read error returns null and
falls through to the default-on path, so file errors *can* silently flip
telemetry on.
2. `chromium-server/navigation.ts` — `navigate()` is reachable from
`POST /api/navigate` with only a `typeof === "string"` check; open-url's
schema is a bare `z.string()`, so the "already validated by zod" premise
never held.
3. `http.ts` — `constantTimeEqual` returns early on a length mismatch,
so the auth token's length is observable.
4. `describe/index.ts:~114` — the ios-remote branch passes `{ isTvOs:
false }` unconditionally, so a remote tvOS simulator takes the iOS
ax-service path, though `isRemoteTvOsSimulator` exists and shake/paste
do use it.
5. `devices/boot-device.ts` — `-crash-report-mode never` is passed
unconditionally *and* appended again by the feature-detecting path, so
every emulator spawn passes it twice.
6. `react-profiler/pipeline/04-rank.ts` — `PARETO_THRESHOLD_PCT` is
dead: `slice(0, 20)` always wins.
7. `reaped-sessions.ts` — a user-facing hint string tells the agent that
`react-profiler-start { force: true }` disposes the debugger and
profiler session; it does not. Left byte-identical because it is a
string literal, not a comment.
8. `utils/simctl-backend.ts` — `localSimctl` is exported with no
importers anywhere.
## Docs
No documentation change is needed: this pass touches only source
comments, and no user-facing capability, tool, CLI flag, config key or
flow-file behaviour changed.
---------
Co-authored-by: filip131311 <f.kaminski2000@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
538 lines
22 KiB
JavaScript
538 lines
22 KiB
JavaScript
/**
|
|
* Pins server.json against the workspace: its version, the npm coordinates it
|
|
* names, and the mcpName ownership proof on both sides.
|
|
*
|
|
* Two layers, because CI runs main(), not the pure function: unit tests over
|
|
* serverJsonMismatches(), then spawned runs of the real script against a
|
|
* miniature repo — the only thing pinning main()'s exit codes, its output and
|
|
* the guard deciding whether it runs at all.
|
|
*
|
|
* Run: node --test scripts/check-workspace-versions.test.mjs
|
|
*/
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
copyFileSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
realpathSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { serverJsonMismatches } from "./check-workspace-versions.mjs";
|
|
|
|
const VERSION = "0.18.0";
|
|
const ARGENT_PKG = { name: "@swmansion/argent", mcpName: "io.github.software-mansion/argent" };
|
|
const SERVER = {
|
|
name: "io.github.software-mansion/argent",
|
|
version: VERSION,
|
|
packages: [{ identifier: "@swmansion/argent", version: VERSION }],
|
|
};
|
|
|
|
/** @param {(s: typeof SERVER) => void} mutate */
|
|
function server(mutate) {
|
|
const copy = structuredClone(SERVER);
|
|
mutate(copy);
|
|
return copy;
|
|
}
|
|
|
|
test("an in-sync pair reports nothing", () => {
|
|
assert.deepEqual(serverJsonMismatches(VERSION, ARGENT_PKG, SERVER), []);
|
|
});
|
|
|
|
test("server.json version drifting from the workspace names both versions", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => (s.version = "0.17.0"))
|
|
);
|
|
assert.deepEqual(problems, [
|
|
"server.json version is 0.17.0, packages/argent/package.json is 0.18.0",
|
|
]);
|
|
});
|
|
|
|
test("a packages[] version npm has never seen is caught", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => (s.packages[0].version = "9.9.9"))
|
|
);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /packages\[0\]\.version is 9\.9\.9/);
|
|
});
|
|
|
|
// Only packages[0] exists today, so nothing else here would notice a check that
|
|
// stopped after the first entry — live the moment a second one is added.
|
|
test("every packages[] entry is validated, not just the first", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => s.packages.push({ identifier: "@attacker/pkg", version: "6.6.6" }))
|
|
);
|
|
assert.deepEqual(problems, [
|
|
"server.json packages[1].version is 6.6.6, packages/argent/package.json is 0.18.0",
|
|
"server.json packages[1].identifier (@attacker/pkg) != published package name (@swmansion/argent)",
|
|
]);
|
|
});
|
|
|
|
test("a packages[] identifier that is not the published package is caught", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => (s.packages[0].identifier = "@swmansion/argnet"))
|
|
);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /packages\[0\]\.identifier \(@swmansion\/argnet\)/);
|
|
});
|
|
|
|
test("a name that no longer matches mcpName is caught", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => (s.name = "com.swmansion/argent"))
|
|
);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /server\.json name \(com\.swmansion\/argent\)/);
|
|
});
|
|
|
|
test("dropping mcpName is caught even though server.json still names it", () => {
|
|
const problems = serverJsonMismatches(VERSION, { name: ARGENT_PKG.name }, SERVER);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /has no mcpName/);
|
|
});
|
|
|
|
// Both sides absent compares equal by ===, so only the presence check catches it.
|
|
test("dropping mcpName and the server.json name together is still caught", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
{ name: ARGENT_PKG.name },
|
|
server((s) => delete s.name)
|
|
);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /has no mcpName/);
|
|
});
|
|
|
|
// The object form is what a hand-edit dropping the brackets produces; it has no
|
|
// .entries(), so this check is what keeps the loop from throwing.
|
|
test("a server.json with no usable packages array is caught", () => {
|
|
const unusable = [
|
|
server((s) => delete s.packages),
|
|
server((s) => (s.packages = [])),
|
|
server(
|
|
(s) =>
|
|
(s.packages = /** @type {any} */ ({ identifier: "@swmansion/argent", version: VERSION }))
|
|
),
|
|
];
|
|
for (const emptied of unusable) {
|
|
const problems = serverJsonMismatches(VERSION, ARGENT_PKG, emptied);
|
|
assert.equal(problems.length, 1);
|
|
assert.match(problems[0], /no packages array/);
|
|
}
|
|
});
|
|
|
|
// With neither side naming a package, the identifier comparison is
|
|
// undefined === undefined and server.json passes pointing at nothing installable.
|
|
test("a packages/argent manifest with no name is caught", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
{ mcpName: ARGENT_PKG.mcpName },
|
|
server((s) => delete s.packages[0].identifier)
|
|
);
|
|
assert.deepEqual(problems, [
|
|
"packages/argent/package.json has no name — server.json's identifier has nothing to point at",
|
|
]);
|
|
});
|
|
|
|
// packages/argent/package.json is the sole source of the version everything else
|
|
// is compared against, so losing it would make every comparison vacuous.
|
|
test("a packages/argent manifest with no version is caught", () => {
|
|
const problems = serverJsonMismatches(undefined, ARGENT_PKG, SERVER);
|
|
assert.deepEqual(problems, [
|
|
"packages/argent/package.json has no version — nothing for server.json to be checked against",
|
|
"server.json packages[0].version is 0.18.0, packages/argent/package.json is undefined",
|
|
]);
|
|
});
|
|
|
|
// Emptying an entry rather than the array leaves the array shape intact, so it
|
|
// reaches the loop that reads .version off it.
|
|
test("a packages[] entry that is not an object is caught", () => {
|
|
for (const [entry, shown] of [
|
|
[null, "null"],
|
|
["@swmansion/argent", "string"],
|
|
// typeof [] === "object", so an array only fails the shape check if tested
|
|
// for separately.
|
|
[["@swmansion/argent", VERSION], "an array"],
|
|
]) {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => (s.packages = [/** @type {any} */ (entry)]))
|
|
);
|
|
assert.deepEqual(problems, [
|
|
`server.json packages[0] is ${shown}, not an object naming a registry and identifier`,
|
|
]);
|
|
}
|
|
});
|
|
|
|
// One stray null must not cost the entries after it, or it would quietly shrink
|
|
// the report the sibling test above relies on being complete.
|
|
test("a non-object packages[] entry does not stop the entries after it", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server(
|
|
(s) =>
|
|
(s.packages = [
|
|
/** @type {any} */ (null),
|
|
{ identifier: "@attacker/pkg", version: "6.6.6" },
|
|
])
|
|
)
|
|
);
|
|
assert.deepEqual(problems, [
|
|
"server.json packages[0] is null, not an object naming a registry and identifier",
|
|
"server.json packages[1].version is 6.6.6, packages/argent/package.json is 0.18.0",
|
|
"server.json packages[1].identifier (@attacker/pkg) != published package name (@swmansion/argent)",
|
|
]);
|
|
});
|
|
|
|
test("every problem is reported at once, not just the first", () => {
|
|
const problems = serverJsonMismatches(
|
|
VERSION,
|
|
ARGENT_PKG,
|
|
server((s) => {
|
|
s.version = "0.17.0";
|
|
s.name = "com.swmansion/argent";
|
|
s.packages[0].version = "0.17.0";
|
|
s.packages[0].identifier = "argent";
|
|
})
|
|
);
|
|
assert.equal(problems.length, 4);
|
|
});
|
|
|
|
// main() is what repo-hygiene.yml runs and nothing above touches it, so a
|
|
// fail-open edit there (an exit(0) on the failure path, a guard that skips
|
|
// main() altogether) would pass every other check in the repo. The tests below
|
|
// run the script for real against a throwaway repo.
|
|
|
|
const SCRIPT_PATH = fileURLToPath(new URL("./check-workspace-versions.mjs", import.meta.url));
|
|
|
|
/**
|
|
* A miniature repo: a copy of the real script, two packages/* manifests and a
|
|
* server.json. Realpath'd because macOS's tmpdir sits behind a /var ->
|
|
* /private/var symlink, which the symlink test reintroduces deliberately.
|
|
* @param {import("node:test").TestContext} t
|
|
* @param {{ argentVersion?: string | null, otherVersion?: string, serverJson?: unknown,
|
|
* argentManifest?: unknown, extraPackages?: Record<string, unknown> }} [options]
|
|
* serverJson: an object written as JSON, a string written verbatim, null omits
|
|
* the file. argentVersion: null omits the version key. argentManifest: replaces
|
|
* packages/argent/package.json wholesale, same convention, overrides
|
|
* argentVersion. extraPackages: further packages/<dir> entries, a null value
|
|
* creating the directory with no package.json in it.
|
|
* @returns {string} the repo root
|
|
*/
|
|
function fixtureRepo(
|
|
t,
|
|
{
|
|
argentVersion = VERSION,
|
|
otherVersion = VERSION,
|
|
serverJson,
|
|
argentManifest,
|
|
extraPackages = {},
|
|
} = {}
|
|
) {
|
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "check-workspace-versions-")));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
mkdirSync(join(root, "scripts"));
|
|
mkdirSync(join(root, "packages", "argent"), { recursive: true });
|
|
mkdirSync(join(root, "packages", "registry"), { recursive: true });
|
|
copyFileSync(SCRIPT_PATH, join(root, "scripts", "check-workspace-versions.mjs"));
|
|
|
|
const write = (/** @type {string} */ path, /** @type {unknown} */ value) =>
|
|
writeFileSync(path, typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
|
|
if (argentManifest === undefined) {
|
|
write(join(root, "packages", "argent", "package.json"), {
|
|
...ARGENT_PKG,
|
|
...(argentVersion === null ? {} : { version: argentVersion }),
|
|
});
|
|
} else if (argentManifest !== null) {
|
|
write(join(root, "packages", "argent", "package.json"), argentManifest);
|
|
}
|
|
write(join(root, "packages", "registry", "package.json"), {
|
|
name: "@argent/registry",
|
|
version: otherVersion,
|
|
});
|
|
for (const [dir, manifest] of Object.entries(extraPackages)) {
|
|
mkdirSync(join(root, "packages", dir), { recursive: true });
|
|
if (manifest !== null) write(join(root, "packages", dir, "package.json"), manifest);
|
|
}
|
|
if (serverJson !== null) write(join(root, "server.json"), serverJson ?? SERVER);
|
|
|
|
return root;
|
|
}
|
|
|
|
/** @param {string} root the path to invoke through, which need not be the real one */
|
|
function runScript(root) {
|
|
return spawnSync(process.execPath, [join(root, "scripts", "check-workspace-versions.mjs")], {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
|
|
/** @param {string} stderr */
|
|
function assertNoStackTrace(stderr) {
|
|
assert.doesNotMatch(stderr, /^\s+at /m, `expected guidance, got a stack trace:\n${stderr}`);
|
|
}
|
|
|
|
test("[script] an in-sync repo passes quietly", (t) => {
|
|
const result = runScript(fixtureRepo(t));
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(result.stderr, "");
|
|
assert.match(result.stdout, /All workspace packages and server\.json are at 0\.18\.0\./);
|
|
});
|
|
|
|
test("[script] server.json drift alone exits 1", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
serverJson: server((s) => {
|
|
s.version = "0.17.0";
|
|
s.packages[0].version = "0.17.0";
|
|
}),
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /server\.json version is 0\.17\.0/);
|
|
assert.match(result.stderr, /server\.json packages\[0\]\.version is 0\.17\.0/);
|
|
assert.match(result.stderr, /Every line above starts with the file to edit\./);
|
|
});
|
|
|
|
// The fixture leaves server.json agreeing with packages/argent, so workspace
|
|
// drift is the only thing that can produce the exit code — that is what makes it
|
|
// pin this branch rather than the server.json one every other failing [script]
|
|
// test also trips.
|
|
test("[script] packages/* drift alone exits 1", (t) => {
|
|
const root = fixtureRepo(t, { otherVersion: "0.17.0" });
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /Workspace package versions are out of sync/);
|
|
assert.match(result.stderr, /0\.17\.0: @argent\/registry/);
|
|
assert.match(result.stderr, /Bump the outliers to match\./);
|
|
assert.doesNotMatch(result.stderr, /server\.json is out of sync/);
|
|
});
|
|
|
|
// The manifest names a package in the report; the directory name is only the
|
|
// fallback for one that does not name itself.
|
|
test("[script] a versioned manifest with no name is reported by its directory", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
otherVersion: "0.17.0",
|
|
extraPackages: { unnamed: { version: "0.16.0" } },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /0\.16\.0: unnamed/);
|
|
});
|
|
|
|
// A manifest with no version is not a workspace package for lockstep purposes, so
|
|
// it must stay out of the version map rather than join it as `undefined` and make
|
|
// every repo look drifted.
|
|
test("[script] a version-less manifest is excluded, not counted as its own version", (t) => {
|
|
const root = fixtureRepo(t, { extraPackages: { noversion: { name: "@argent/noversion" } } });
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 0, `expected a clean repo, got:\n${result.stdout}${result.stderr}`);
|
|
assert.equal(result.stderr, "");
|
|
});
|
|
|
|
// packages/docs carries a standalone version (0.0.0 in the real repo) and is
|
|
// excluded from lockstep by NON_WORKSPACE_DIRS; without that skip it would read
|
|
// as drift on every release.
|
|
test("[script] packages/docs is excluded from lockstep, not read as drift", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
extraPackages: { docs: { name: "@argent/docs", version: "0.0.0" } },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 0, `expected a clean repo, got:\n${result.stdout}${result.stderr}`);
|
|
assert.equal(result.stderr, "");
|
|
});
|
|
|
|
// A manifest that exists but cannot be parsed is not the same as a directory with
|
|
// no manifest: dropping it silently would take a real package out of the
|
|
// comparison and pass a workspace that had drifted.
|
|
test("[script] an unparseable packages/* manifest fails instead of being skipped", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
extraPackages: { broken: '{ "name": "@argent/broken", "version": "9.9.9",, }' },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /Cannot read packages\/broken\/package\.json: /);
|
|
assert.match(result.stderr, /git checkout -- packages\/broken\/package\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
});
|
|
|
|
// packages/argent-private is exactly this in the real repo: a submodule directory
|
|
// carrying no package.json. The scan has to step over it and keep going, or every
|
|
// package sorting after it silently leaves the comparison.
|
|
test("[script] an unusable packages/* directory does not stop the scan", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
otherVersion: "0.17.0",
|
|
extraPackages: { nomanifest: null, noversion: { name: "@argent/noversion" } },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /0\.17\.0: @argent\/registry/);
|
|
});
|
|
|
|
// JSON.parse succeeds on a file holding `null`, a bare scalar or an array, so none
|
|
// of them reach the scan's try/catch — yet each is a manifest that is there but
|
|
// unusable, and skipping one would drop a real package out of the lockstep
|
|
// comparison and pass a workspace that had drifted.
|
|
test("[script] a packages/* manifest that is not an object fails on-message, not silently skipped", (t) => {
|
|
for (const [content, shape] of [
|
|
["null", "null"],
|
|
['"0.18.0"', "string"],
|
|
['["@swmansion/argent", "0.18.0"]', "an array"],
|
|
]) {
|
|
const root = fixtureRepo(t, {
|
|
otherVersion: "0.17.0",
|
|
extraPackages: { corrupt: content },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(
|
|
result.status,
|
|
1,
|
|
`expected a failure for ${content}, got:\n${result.stdout}${result.stderr}`
|
|
);
|
|
assert.equal(
|
|
result.stderr.split("\n")[0],
|
|
`packages/corrupt/package.json is ${shape}, not a JSON object`
|
|
);
|
|
assert.match(result.stderr, /git checkout -- packages\/corrupt\/package\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
}
|
|
});
|
|
|
|
// The report groups every package sharing a version onto one alphabetised line.
|
|
// Directory `aaa` holds a package named `@zzz/...`, so the scan reaches it first
|
|
// and the names come out in the opposite order unless they are sorted.
|
|
test("[script] the drift report lists every package sharing a version, sorted", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
otherVersion: "0.17.0",
|
|
extraPackages: { aaa: { name: "@zzz/scanned-first", version: VERSION } },
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /0\.18\.0: @swmansion\/argent, @zzz\/scanned-first\n/);
|
|
});
|
|
|
|
// The one shape where every comparison the script makes is vacuously satisfied:
|
|
// with no version on the manifest and none in server.json, each `!==` compares
|
|
// undefined against undefined.
|
|
test("[script] a version-less manifest fails instead of passing vacuously", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
argentVersion: null,
|
|
serverJson: server((s) => {
|
|
delete s.version;
|
|
delete s.packages[0].version;
|
|
}),
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /packages\/argent\/package\.json has no version/);
|
|
assert.doesNotMatch(result.stdout, /at undefined/);
|
|
});
|
|
|
|
test("[script] a half-finished bump reports packages/* and server.json in one run", (t) => {
|
|
const root = fixtureRepo(t, {
|
|
otherVersion: "0.17.0",
|
|
serverJson: server((s) => {
|
|
s.version = "0.17.0";
|
|
s.packages[0].version = "0.17.0";
|
|
}),
|
|
});
|
|
const result = runScript(root);
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /Workspace package versions are out of sync/);
|
|
assert.match(result.stderr, /@argent\/registry/);
|
|
assert.match(result.stderr, /server\.json version is 0\.17\.0/);
|
|
});
|
|
|
|
// node realpaths the main module before setting import.meta.url but leaves
|
|
// process.argv[1] as typed, so a self-invocation guard comparing them raw would
|
|
// skip main() here and exit 0 — a drifted repo looking like a clean one.
|
|
test("[script] a run through a symlinked path still catches drift", (t) => {
|
|
const root = fixtureRepo(t, { serverJson: server((s) => (s.version = "0.17.0")) });
|
|
const aliasParent = realpathSync(mkdtempSync(join(tmpdir(), "check-workspace-versions-alias-")));
|
|
t.after(() => rmSync(aliasParent, { recursive: true, force: true }));
|
|
const alias = join(aliasParent, "repo");
|
|
symlinkSync(root, alias, "dir");
|
|
|
|
const result = runScript(alias);
|
|
assert.equal(result.status, 1, `expected a failure, got:\n${result.stdout}${result.stderr}`);
|
|
assert.match(result.stderr, /server\.json version is 0\.17\.0/);
|
|
});
|
|
|
|
test("[script] a missing server.json fails on-message", (t) => {
|
|
const result = runScript(fixtureRepo(t, { serverJson: null }));
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /Cannot read server\.json: /);
|
|
assert.match(result.stderr, /git checkout -- server\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
});
|
|
|
|
test("[script] a corrupt server.json does not swallow the packages/* drift", (t) => {
|
|
const result = runScript(fixtureRepo(t, { otherVersion: "0.17.0", serverJson: null }));
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /Workspace package versions are out of sync/);
|
|
assert.match(result.stderr, /Cannot read server\.json/);
|
|
});
|
|
|
|
test("[script] a malformed server.json fails on-message", (t) => {
|
|
const result = runScript(fixtureRepo(t, { serverJson: "{ not json" }));
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /server\.json is not valid JSON: /);
|
|
assert.match(result.stderr, /git checkout -- server\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
});
|
|
|
|
// `null`, a bare scalar and an array all parse cleanly, so the read has to reject
|
|
// them itself — every field lookup after would throw or quietly match undefined.
|
|
test("[script] a server.json that parses but is not an object fails on-message", (t) => {
|
|
for (const [content, shape] of [
|
|
["null", "null"],
|
|
['"0.18.0"', "string"],
|
|
["[]", "an array"],
|
|
]) {
|
|
const result = runScript(fixtureRepo(t, { serverJson: content }));
|
|
assert.equal(result.status, 1, `expected a failure for ${content}, got:\n${result.stdout}`);
|
|
assert.equal(result.stderr.split("\n")[0], `server.json is ${shape}, not a JSON object`);
|
|
assert.match(result.stderr, /git checkout -- server\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
}
|
|
});
|
|
|
|
// readTrackedJson has two call sites and every test above drives only the
|
|
// server.json one, so nothing observed that packages/argent/package.json is read
|
|
// the same hardened way. (Its malformed-JSON arm is unreachable from this side —
|
|
// the scan reads the same file first and stops there — so these two cover the
|
|
// arms that are.)
|
|
test("[script] a missing packages/argent/package.json fails on-message", (t) => {
|
|
const result = runScript(fixtureRepo(t, { argentManifest: null }));
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /Cannot read packages\/argent\/package\.json: /);
|
|
assert.match(result.stderr, /git checkout -- packages\/argent\/package\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
});
|
|
|
|
test("[script] a packages/argent/package.json that is not an object fails on-message", (t) => {
|
|
const result = runScript(fixtureRepo(t, { argentManifest: "null" }));
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /packages\/argent\/package\.json is null, not a JSON object/);
|
|
assert.match(result.stderr, /git checkout -- packages\/argent\/package\.json/);
|
|
assertNoStackTrace(result.stderr);
|
|
});
|