Files
gods-eye-view/src/pinokioUpdatePreview.test.mjs
T
Bilawal SidhuandPedro Lobato 70dca86bc1 fix(pinokio): disclose incoming changes before Update pulls and installs (#507)
Supersedes #356 by @Lob26, ported onto the current component layout with fixes from review.

Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.

Co-authored-by: Pedro Lobato <[email protected]>
2026-09-14 19:34:23 -05:00

448 lines
16 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, writeFileSync } from 'node:fs';
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import path from 'node:path';
import {
readGit,
redactRemoteUrl,
reportIncomingChanges,
runPinokioUpdate,
updateFromRemote,
} from '../scripts/pinokio-update.mjs';
const source = readFileSync(
new URL('../scripts/pinokio-update.mjs', import.meta.url),
'utf8',
);
/** Run git in a fixture repository, failing the test on a git error. */
function git(cwd, args) {
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
assert.equal(
result.status,
0,
`git ${args.join(' ')} failed: ${result.stderr || result.error}`,
);
return result.stdout.trim();
}
/** Escape a path so it can be matched literally inside a RegExp. */
const literal = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/**
* Build a disposable origin repository and a clone that tracks it.
*
* Everything is on disk and driven through real git, so the assertions are
* about what the update path does to a checkout rather than about a transcript
* of injected strings.
*
* @param {import('node:test').TestContext} t - Owns the fixture's cleanup.
*/
async function fixture(t) {
const scratch = await realpath(
await mkdtemp(path.join(tmpdir(), 'gev-update-')),
);
t.after(() => rm(scratch, { recursive: true, force: true }));
const origin = path.join(scratch, 'origin');
const clone = path.join(scratch, 'clone');
git(scratch, ['init', '--quiet', '--initial-branch=main', origin]);
git(origin, ['config', 'user.email', '[email protected]']);
git(origin, ['config', 'user.name', 'Fixture']);
await writeFile(path.join(origin, 'README.md'), 'first\n');
git(origin, ['add', 'README.md']);
git(origin, ['commit', '--quiet', '-m', 'first commit']);
git(scratch, ['clone', '--quiet', origin, clone]);
git(clone, ['config', 'user.email', '[email protected]']);
git(clone, ['config', 'user.name', 'Fixture']);
/** Add one commit to the origin repository. */
const push = async (message, contents) => {
await writeFile(path.join(origin, 'README.md'), contents);
git(origin, ['add', 'README.md']);
git(origin, ['commit', '--quiet', '-m', message]);
return git(origin, ['rev-parse', 'HEAD']);
};
/** Same, from a synchronous callback (used to move the remote mid-update). */
const pushSync = (message, contents) => {
writeFileSync(path.join(origin, 'README.md'), contents);
git(origin, ['add', 'README.md']);
git(origin, ['commit', '--quiet', '-m', message]);
return git(origin, ['rev-parse', 'HEAD']);
};
const out = [];
const io = {
cwd: clone,
log: (line) => out.push(String(line)),
warn: (line) => out.push(String(line)),
fetchRemote: (remote) => git(clone, ['fetch', '--quiet', remote]),
apply: (args) => git(clone, args),
};
return {
scratch,
origin,
clone,
push,
pushSync,
io,
out,
text: () => out.join('\n'),
};
}
test('a checkout with no upstream is reported and left to the plain pull', async (t) => {
const app = await fixture(t);
git(app.clone, ['checkout', '--quiet', '-b', 'local-only']);
let fetched = null;
const plan = reportIncomingChanges({
...app.io,
fetchRemote: (remote) => {
fetched = remote;
},
});
assert.deepEqual(plan, { apply: null, fallback: true });
assert.match(app.text(), /No upstream branch is configured/);
assert.equal(fetched, null, 'nothing is fetched without an upstream');
});
test('the tracking branch and the remote it fetches from are printed', async (t) => {
const app = await fixture(t);
await app.push('second commit', 'second\n');
reportIncomingChanges(app.io);
assert.match(app.text(), /Tracking origin\/main/);
assert.match(app.text(), new RegExp(`Fetching from ${literal(app.origin)}`));
});
test('the incoming commits and their diffstat are printed, and the report alone applies nothing', async (t) => {
const app = await fixture(t);
const before = git(app.clone, ['rev-parse', 'HEAD']);
await app.push('second commit', 'second\n');
await app.push('third commit', 'third\n');
const plan = reportIncomingChanges(app.io);
const text = app.text();
assert.match(text, /2 incoming commit\(s\)/);
assert.match(text, /second commit/);
assert.match(text, /third commit/);
assert.match(text, /README\.md \| 2 \+-/);
assert.equal(plan.fallback, false);
assert.equal(
git(app.clone, ['rev-parse', 'HEAD']),
before,
'the report must not move the checkout',
);
});
test('the revision that is applied is the revision that was disclosed', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
const plan = reportIncomingChanges(app.io);
// Something lands on the remote between the disclosure and the apply. A
// second fetch would install it without ever naming it.
await app.push('a commit nobody was shown', 'fourth\n');
assert.equal(plan.apply, disclosed);
app.io.apply(['merge', '--ff-only', plan.apply]);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), disclosed);
assert.match(app.text(), new RegExp(`Applying ${disclosed}`));
});
test('the whole update applies exactly one disclosed revision', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
const plan = updateFromRemote(app.io);
assert.equal(plan.apply, disclosed);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), disclosed);
});
test('a remote that moves during the update does not change what is applied', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
let later = null;
const plan = updateFromRemote({
...app.io,
fetchRemote: (remote) => {
git(app.clone, ['fetch', '--quiet', remote]);
// The remote gains a commit after the one fetch this update makes. Only
// a second fetch — which there must not be — could reach it.
later = app.pushSync('a commit nobody was shown', 'fourth\n');
},
});
assert.equal(plan.apply, disclosed);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), disclosed);
assert.notEqual(git(app.clone, ['rev-parse', 'HEAD']), later);
});
test('an up-to-date checkout says so and applies nothing', async (t) => {
const app = await fixture(t);
const before = git(app.clone, ['rev-parse', 'HEAD']);
const plan = updateFromRemote(app.io);
assert.deepEqual(plan, { apply: null, fallback: false });
assert.match(app.text(), /Already up to date/);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), before);
});
test('an unreadable commit range is reported as unreadable, not as up to date', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
const plan = reportIncomingChanges({
...app.io,
read: (args) =>
args[0] === 'log' ? null : readGit(args, { cwd: app.clone }),
});
const text = app.text();
assert.doesNotMatch(text, /Already up to date/);
assert.match(text, /Could not list the commits/);
assert.equal(plan.apply, disclosed, 'the fetched revision is still applied');
});
test('an upstream that cannot be resolved after fetching stops the update', async (t) => {
const app = await fixture(t);
const before = git(app.clone, ['rev-parse', 'HEAD']);
await app.push('second commit', 'second\n');
const plan = updateFromRemote({
...app.io,
// The target ref resolves to nothing after the fetch — a tracking ref
// pruned out from under the run.
read: (args) =>
args[0] === 'rev-parse' &&
args.length === 2 &&
args[1].startsWith('refs/')
? null
: readGit(args, { cwd: app.clone }),
});
assert.deepEqual(plan, { apply: null, fallback: false, stopped: true });
assert.doesNotMatch(app.text(), /Already up to date/);
assert.match(app.text(), /Could not resolve origin\/main after fetching/);
assert.match(app.text(), /Stopping/);
// Nothing may be applied: a fallback pull here would fetch again and could
// install a revision this run never resolved.
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), before);
});
test('a remote whose name contains a slash is read correctly', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
git(app.clone, ['remote', 'rename', 'origin', 'team/origin']);
git(app.clone, ['fetch', '--quiet', 'team/origin']);
git(app.clone, ['branch', '--set-upstream-to=team/origin/main', 'main']);
const plan = updateFromRemote({
...app.io,
fetchRemote: (remote) => {
assert.equal(remote, 'team/origin');
git(app.clone, ['fetch', '--quiet', remote]);
},
});
assert.equal(plan.apply, disclosed);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), disclosed);
assert.match(app.text(), new RegExp(`Fetching from ${literal(app.origin)}`));
});
test('a diffstat that cannot be read is reported, and the commits still are', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
const plan = reportIncomingChanges({
...app.io,
read: (args) =>
args[0] === 'diff' ? null : readGit(args, { cwd: app.clone }),
});
assert.equal(plan.apply, disclosed);
assert.match(app.text(), /1 incoming commit\(s\)/);
assert.match(app.text(), /Could not read the diffstat/);
assert.doesNotMatch(app.text(), /Files affected/);
});
test('diverged history is refused rather than applied', async (t) => {
const app = await fixture(t);
await app.push('second commit', 'second\n');
await writeFile(path.join(app.clone, 'README.md'), 'local work\n');
git(app.clone, ['add', 'README.md']);
git(app.clone, ['commit', '--quiet', '-m', 'local commit']);
const before = git(app.clone, ['rev-parse', 'HEAD']);
const plan = reportIncomingChanges(app.io);
const refused = spawnSync('git', ['merge', '--ff-only', plan.apply], {
cwd: app.clone,
encoding: 'utf8',
});
assert.notEqual(refused.status, 0, 'a fast-forward-only merge must refuse');
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), before);
});
test('a remote that is behind this checkout is named, not announced as incoming', async (t) => {
const app = await fixture(t);
await writeFile(path.join(app.clone, 'README.md'), 'ahead\n');
git(app.clone, ['add', 'README.md']);
git(app.clone, ['commit', '--quiet', '-m', 'local commit']);
const plan = reportIncomingChanges(app.io);
assert.match(app.text(), /is not ahead of this checkout/);
assert.doesNotMatch(app.text(), /incoming commit/);
assert.ok(plan.apply, 'the fetched revision is still named');
});
test('a stop means nothing is installed, and the script fails', async (t) => {
const app = await fixture(t);
const before = git(app.clone, ['rev-parse', 'HEAD']);
await app.push('second commit', 'second\n');
const installs = [];
const failures = [];
const plan = runPinokioUpdate({
...app.io,
read: (args) =>
args[0] === 'rev-parse' &&
args.length === 2 &&
args[1].startsWith('refs/')
? null
: readGit(args, { cwd: app.clone }),
install: () => installs.push('install'),
fail: (code) => failures.push(code),
});
assert.equal(plan.stopped, true);
// Installing here would run install scripts and report success for an update
// that never happened.
assert.deepEqual(installs, []);
assert.deepEqual(failures, [1]);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), before);
});
test('an ordinary update installs once and does not fail', async (t) => {
const app = await fixture(t);
const disclosed = await app.push('second commit', 'second\n');
const installs = [];
const failures = [];
const plan = runPinokioUpdate({
...app.io,
install: () => installs.push('install'),
fail: (code) => failures.push(code),
});
assert.equal(plan.apply, disclosed);
assert.equal(git(app.clone, ['rev-parse', 'HEAD']), disclosed);
assert.deepEqual(installs, ['install']);
assert.deepEqual(failures, []);
});
test('an up-to-date checkout still reinstalls', async (t) => {
const app = await fixture(t);
const installs = [];
const plan = runPinokioUpdate({
...app.io,
install: () => installs.push('install'),
fail: () => assert.fail('an up-to-date checkout is not a failure'),
});
assert.deepEqual(plan, { apply: null, fallback: false });
assert.deepEqual(installs, ['install']);
});
test('the entrypoint is what the direct invocation runs', () => {
const guarded = source.slice(source.indexOf('if (isDirectInvocation('));
assert.match(guarded, /runPinokioUpdate\(\);/);
// The install must not sit beside it in the guard, where a stop could not
// prevent it.
assert.doesNotMatch(guarded, /installPinokioDependencies\(\)/);
});
test('a password in the remote URL is not printed', () => {
assert.equal(
redactRemoteUrl('https://someone:[email protected]/org/repo.git'),
'https://***@github.com/org/repo.git',
);
});
test('a bare token in the remote URL is not printed', () => {
const redacted = redactRemoteUrl(
'https://[email protected]/org/repo.git',
);
assert.doesNotMatch(redacted, /ghp_secret/);
assert.match(redacted, /github\.com\/org\/repo\.git/);
});
test('a credential in a query parameter is not printed', () => {
assert.equal(
redactRemoteUrl('https://host.example/org/repo.git?access_token=secret'),
'https://host.example/org/repo.git',
);
// The whole query goes, so a parameter name nobody thought of goes with it.
assert.equal(
redactRemoteUrl('https://host.example/org/repo.git?sneaky=secret&x=1#frag'),
'https://host.example/org/repo.git',
);
assert.equal(
redactRemoteUrl('https://user:[email protected]/org/repo.git?token=secret'),
'https://***@host.example/org/repo.git',
);
});
test('an ordinary remote URL is printed unchanged', () => {
const url = 'https://github.com/bilawalsidhu/gods-eye-view.git';
assert.equal(redactRemoteUrl(url), url);
assert.equal(
redactRemoteUrl('[email protected]:bilawalsidhu/gods-eye-view.git'),
'[email protected]:bilawalsidhu/gods-eye-view.git',
);
assert.equal(redactRemoteUrl(null), null);
assert.equal(redactRemoteUrl(''), null);
});
test('a credential-bearing remote is redacted in the printed report', async (t) => {
const app = await fixture(t);
await app.push('second commit', 'second\n');
reportIncomingChanges({
...app.io,
read: (args) =>
args[0] === 'remote'
? 'https://someone:[email protected]/org/repo.git'
: readGit(args, { cwd: app.clone }),
});
assert.doesNotMatch(app.text(), /ghp_secret/);
assert.match(
app.text(),
/Fetching from https:\/\/\*\*\*@github\.com\/org\/repo\.git/,
);
});
test('a failed git read is null, and an empty successful read is an empty string', async (t) => {
const app = await fixture(t);
assert.equal(
readGit(['rev-parse', '--verify', 'refs/heads/absent'], { cwd: app.clone }),
null,
);
assert.equal(
readGit(['log', '--oneline', 'HEAD..HEAD'], { cwd: app.clone }),
'',
);
});
test('the update and reinstall stay behind the direct-invocation guard', () => {
// Without the guard, importing this module to test it would update the
// checkout and run npm ci as an import side effect.
assert.match(
source,
/if \(isDirectInvocation\(process\.argv\[1\], MODULE_PATH\)\) \{/,
);
const entrypoint = source.slice(
source.indexOf('export function runPinokioUpdate'),
);
// Order is the whole point of the fix: disclose and apply, then install.
assert.ok(
entrypoint.indexOf('updateFromRemote(io)') <
entrypoint.indexOf('install()'),
'the update must be disclosed and applied before dependencies are installed',
);
});
test('the disclosed revision is applied by merge, not by a second pull', () => {
const applier = source.slice(
source.indexOf('export function updateFromRemote'),
);
assert.match(applier, /apply\(\['merge', '--ff-only', plan\.apply\]\)/);
// The plain pull remains, but only on the path where nothing was disclosed.
assert.match(
applier,
/else if \(plan\.fallback\) apply\(\['pull', '--ff-only'\]\)/,
);
});