mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
test: bind installed native payloads and await saved state
Isolate actual Codex resolver fixtures from global bridge lookup and preserve host qualification blockers with literal evidence. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
123a188374
commit
598e190cb3
6 files changed
+133
-19
No files matched your search
@@ -93,6 +93,42 @@ Next action: freeze this corrected tooling and run one remote ARM host treatment
|
||||
against the same verified archives. Require actual host startup before the final
|
||||
four-target qualification; retain literal product/tool source identities.
|
||||
|
||||
The [host treatment](https://github.com/paperclipai/paperclip/actions/runs/38276826878)
|
||||
at tooling `123a18837478d8cd0e2aff23f7e7aa86c9107de3` also fails native startup.
|
||||
The actual ARM host reaches the installed daemon and Codex, then bubblewrap
|
||||
fails loopback `RTM_NEWADDR` with `Operation not permitted`. No successful
|
||||
native session or after-start continuity is claimed. Docker/host installation
|
||||
identity passed. Exact upstream Codex 0.160 source places namespace creation
|
||||
before its own seccomp filter and always restricts networking for filesystem
|
||||
helpers. A Paperclip network-mode change would not solve this. Prepare one
|
||||
temporary same-policy direct Codex control plus read-only host namespace facts
|
||||
to distinguish host enforcement from native mediation. Do not alter kernel,
|
||||
Docker, or product permission policy. Receipts:
|
||||
`/private/tmp/ci-final-arm-host-diagnostic-123-receipt.json` and
|
||||
`/private/tmp/codex-default-arm-sandbox-source-diagnosis.json`.
|
||||
|
||||
Fresh `123a188` review is 5/5 with one payload-continuity finding. The existing
|
||||
consumer now hashes the actual official Codex native executable separately
|
||||
from its launcher, checks containment/header, and compares both across Docker,
|
||||
host, and before/after. Twenty-one controls pass, including native-only mutation
|
||||
and an older supported package layout. Receipt:
|
||||
`/private/tmp/pr2-native-codex-payload-and-control-preparation-receipt.json`.
|
||||
|
||||
Ordinary `123a188` CI found two test gaps. Nine new login fixture controls were
|
||||
shadowed by Vitest's global module bridge. The existing bounded plain-Node
|
||||
resolver seam now proves private package selection and containment before only
|
||||
fake login; all 27 login cases and typecheck pass. A repository test reloaded one
|
||||
millisecond after Save, before any update request reached the server. It now
|
||||
awaits the real saved-value render, then retains the persistence assertion after
|
||||
reload. No product repository code changes. Receipts:
|
||||
`/private/tmp/codex-default-login-resolver-isolation-receipt.json` and
|
||||
`/private/tmp/ci-final-123a188-repository-e2e-analysis.json`.
|
||||
|
||||
Next action: freeze these review/test fixes, run the bounded direct ARM control,
|
||||
and require fresh Linux CI. Keep ARM startup sign-off unresolved until proven
|
||||
on a compatible target; package installation and direct execution alone do not
|
||||
qualify the native runner. All Docker/native builds remain remote.
|
||||
|
||||
## Current addition: Linux ARM64
|
||||
|
||||
The user now requires ARM Linux support as well. Treat ARM Linux as ARM64
|
||||
|
||||
@@ -15,7 +15,7 @@ const paths = { assets: '/private/staging/assets', consumer: '/private/staging/c
|
||||
const values = (args, flag) => args.flatMap((value, index) => value === flag ? [args[index + 1]] : []);
|
||||
|
||||
test('native host startup preserves the consumer lock and both executable byte identities', () => {
|
||||
const before = { lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), codexCommandSha256: 'c'.repeat(64) };
|
||||
const before = { lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), codexCommandSha256: 'c'.repeat(64), codexNativeExecutableSha256: 'e'.repeat(64) };
|
||||
assert.doesNotThrow(() => assertInstalledNativeConsumerContinuity(before, { ...before }));
|
||||
for (const field of Object.keys(before)) {
|
||||
assert.throws(() => assertInstalledNativeConsumerContinuity(before, { ...before, [field]: 'd'.repeat(64) }), new RegExp(field));
|
||||
@@ -252,6 +252,14 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
const platformPath = `node_modules/@openai/codex-${target}`;
|
||||
const platform = join(consumer, platformPath); mkdirSync(platform, { recursive: true });
|
||||
const nativeVendor = join(platform, 'vendor'); mkdirSync(nativeVendor);
|
||||
const triple = { 'linux-x64': 'x86_64-unknown-linux-musl', 'linux-arm64': 'aarch64-unknown-linux-musl',
|
||||
'darwin-x64': 'x86_64-apple-darwin', 'darwin-arm64': 'aarch64-apple-darwin', 'win32-x64': 'x86_64-pc-windows-msvc', 'win32-arm64': 'aarch64-pc-windows-msvc' }[target];
|
||||
const nativeBytes = Buffer.alloc(64);
|
||||
if (process.platform === 'darwin') { nativeBytes.writeUInt32LE(0xfeedfacf); nativeBytes.writeUInt32LE(process.arch === 'arm64' ? 0x0100000c : 0x01000007, 4); }
|
||||
else if (process.platform === 'win32') { nativeBytes.set([0x4d, 0x5a]); nativeBytes.writeUInt32LE(40, 60); nativeBytes.writeUInt32LE(0x00004550, 40); nativeBytes.writeUInt16LE(process.arch === 'arm64' ? 0xaa64 : 0x8664, 44); }
|
||||
else { nativeBytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); nativeBytes.writeUInt16LE(process.arch === 'arm64' ? 183 : 62, 18); }
|
||||
const nativeFile = join(nativeVendor, triple, 'bin', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(nativeVendor, triple, 'bin'), { recursive: true }); writeFileSync(nativeFile, nativeBytes, { mode: 0o755 });
|
||||
const manifest = { name: '@openai/codex', version: `0.160.0-${target}`, os: [process.platform], cpu: [process.arch] };
|
||||
writeFileSync(join(platform, 'package.json'), JSON.stringify(manifest));
|
||||
const entry = { name: manifest.name, version: manifest.version,
|
||||
@@ -260,6 +268,8 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
writeFileSync(join(consumer, 'package-lock.json'), JSON.stringify(lock));
|
||||
const server = join(consumer, 'node_modules/@paperclipai/server');
|
||||
const vendor = join(server, 'dist/vendor/paperclip-runner'), drivers = join(vendor, 'drivers/acpx'); mkdirSync(drivers, { recursive: true });
|
||||
mkdirSync(join(vendor, 'live'));
|
||||
writeFileSync(join(vendor, 'live/runner-binary.js'), `export const runnerBinaryTarget = ${runnerBinaryTarget.toString()};`);
|
||||
writeFileSync(join(server, 'package.json'), JSON.stringify({ name: '@paperclipai/server', type: 'module' }));
|
||||
const integrity = join(drivers, 'installation-integrity.js');
|
||||
const security = join(vendor, 'drivers/codex/codex-security-config.js'); mkdirSync(join(vendor, 'drivers/codex'));
|
||||
@@ -285,6 +295,23 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
assert.equal(JSON.parse(result.stdout).codexPlatformPackageSource, 'official npm');
|
||||
assert.equal(JSON.parse(result.stdout).codexConsumerPlatform, target);
|
||||
assert.equal(JSON.parse(result.stdout).codexCommandSha256, createHash('sha256').update(readFileSync(command)).digest('hex'));
|
||||
assert.equal(JSON.parse(result.stdout).codexNativeExecutableSha256, createHash('sha256').update(nativeBytes).digest('hex'));
|
||||
const original = JSON.parse(result.stdout);
|
||||
nativeBytes[63] = 1; writeFileSync(nativeFile, nativeBytes);
|
||||
result = run(); assert.equal(result.status, 0, result.stderr);
|
||||
const changed = JSON.parse(result.stdout);
|
||||
assert.equal(changed.codexCommandSha256, original.codexCommandSha256, 'native mutation does not change the npm launcher');
|
||||
assert.notEqual(changed.codexNativeExecutableSha256, original.codexNativeExecutableSha256);
|
||||
assert.throws(() => assertInstalledNativeConsumerContinuity({ lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), ...original },
|
||||
{ lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), ...changed }), /codexNativeExecutableSha256/);
|
||||
nativeBytes[63] = 0; writeFileSync(nativeFile, nativeBytes);
|
||||
rmSync(nativeFile);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /one native executable/);
|
||||
const foreignNative = join(root, 'foreign-codex'); writeFileSync(foreignNative, nativeBytes, { mode: 0o755 });
|
||||
symlinkSync(foreignNative, nativeFile);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /bounded regular file in its selected platform package/);
|
||||
rmSync(nativeFile); writeFileSync(nativeFile, nativeBytes, { mode: 0o755 });
|
||||
result = run(); assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, true);
|
||||
assert.equal(JSON.parse(result.stdout).codexCommandLeaseVerified, true);
|
||||
assert.equal(JSON.parse(result.stdout).codexNativeSandboxResourcesVerified, true);
|
||||
@@ -308,6 +335,8 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
// Native Codex uses protocol compatibility, not the ACPX version allowlist.
|
||||
// Keep exact official-npm host provenance and the selected resource boundary.
|
||||
mkdirSync(join(legacy, 'vendor'));
|
||||
const legacyNative = join(legacy, 'vendor', triple, 'codex', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(legacy, 'vendor', triple, 'codex'), { recursive: true }); writeFileSync(legacyNative, nativeBytes, { mode: 0o755 });
|
||||
writeFileSync(legacyCommand, '#!/bin/sh\nprintf "codex-cli 0.156.1\\n"\n', { mode: 0o755 });
|
||||
writeFileSync(security, securitySource.replaceAll(JSON.stringify(nativeVendor), JSON.stringify(join(legacy, 'vendor'))));
|
||||
writeFileSync(probe, installedCodexProbeSource(index, consumer, undefined, { nativeCodex: true }));
|
||||
|
||||
@@ -160,6 +160,21 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
|
||||
const platformRoot = realpathSync(dirname(selectedPlatformManifest));
|
||||
const nativeVendor = realpathSync(join(platformRoot, 'vendor'));
|
||||
assert.ok(contained(nativeVendor) && nativeVendor.startsWith(platformRoot + sep), 'Codex vendor resources must remain inside their installed platform package');
|
||||
const triple = { 'linux-x64': 'x86_64-unknown-linux-musl', 'linux-arm64': 'aarch64-unknown-linux-musl',
|
||||
'darwin-x64': 'x86_64-apple-darwin', 'darwin-arm64': 'aarch64-apple-darwin',
|
||||
'win32-x64': 'x86_64-pc-windows-msvc', 'win32-arm64': 'aarch64-pc-windows-msvc' }[process.platform + '-' + process.arch];
|
||||
assert.ok(triple, 'Codex native payload requires a known host target');
|
||||
const nativeName = process.platform === 'win32' ? 'codex.exe' : 'codex';
|
||||
const nativeCandidates = ['bin', 'codex'].map(directory => join(nativeVendor, triple, directory, nativeName)).filter(existsSync);
|
||||
assert.equal(nativeCandidates.length, 1, 'Selected official Codex platform must contain one native executable');
|
||||
const nativeExecutable = realpathSync(nativeCandidates[0]);
|
||||
const nativeStat = lstatSync(nativeCandidates[0]);
|
||||
assert.ok(nativeExecutable.startsWith(nativeVendor + sep) && nativeStat.isFile() && !nativeStat.isSymbolicLink()
|
||||
&& nativeStat.size >= 20 && nativeStat.size <= 512 * 1024 * 1024, 'Codex native executable must be a bounded regular file in its selected platform package');
|
||||
accessSync(nativeExecutable, constants.X_OK);
|
||||
const nativeBytes = readFileSync(nativeExecutable);
|
||||
const { runnerBinaryTarget } = await import(pathToFileURL(join(vendor, 'live/runner-binary.js')).href);
|
||||
assert.equal(runnerBinaryTarget(nativeBytes), process.platform + '-' + process.arch, 'Codex native executable must match the actual host architecture');
|
||||
const sandboxSource = { HOME: '/private-provider-home', PATH: '/usr/local/bin:/usr/bin:/bin' };
|
||||
const readRoots = codexExecutableReadOnlyRoots(sandboxSource, command);
|
||||
assert.ok(readRoots.includes(nativeVendor), 'Selected native Codex vendor directory must be readable inside its sandbox');
|
||||
@@ -176,6 +191,8 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
|
||||
await lease.close();`}
|
||||
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, installedCodexVersion: version,
|
||||
codexCommandSha256: createHash('sha256').update(readFileSync(command)).digest('hex'),
|
||||
codexNativeExecutableSha256: createHash('sha256').update(nativeBytes).digest('hex'),
|
||||
codexNativeExecutablePath: relative(root, nativeExecutable).split(sep).join('/'),
|
||||
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version,
|
||||
codexNativeSandboxResourcesVerified: true,
|
||||
${nativeCodex ? '' : `pinnedCodexVersion: ${JSON.stringify(expectedVersion)}, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true,`}
|
||||
@@ -185,7 +202,7 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
|
||||
|
||||
/** Installed bytes and consumer resolution must survive host protocol startup. */
|
||||
export function assertInstalledNativeConsumerContinuity(before, after) {
|
||||
for (const field of ['lockSha256', 'runnerSha256', 'codexCommandSha256']) {
|
||||
for (const field of ['lockSha256', 'runnerSha256', 'codexCommandSha256', 'codexNativeExecutableSha256']) {
|
||||
if (!/^[a-f0-9]{64}$/.test(before[field]) || before[field] !== after[field]) {
|
||||
throw new Error(`Installed native host startup changed ${field}`);
|
||||
}
|
||||
|
||||
@@ -166,13 +166,16 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP
|
||||
const hostDaemon = () => hostProbe('host-daemon-probe.mjs', installedDaemonProbeSource(hostInstalled, consumer, sourceRevision));
|
||||
const hostCodex = () => hostProbe('host-codex-probe.mjs', installedCodexProbeSource(join(hostInstalled, 'index.js'), consumer, undefined, { nativeCodex: true }));
|
||||
const digest = value => createHash('sha256').update(value).digest('hex');
|
||||
const before = { lockSha256: digest(lock), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: hostCodex().codexCommandSha256 };
|
||||
const beforeCodex = hostCodex();
|
||||
const before = { lockSha256: digest(lock), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: beforeCodex.codexCommandSha256, codexNativeExecutableSha256: beforeCodex.codexNativeExecutableSha256 };
|
||||
assert.equal(before.runnerSha256, daemon.sha256.slice(7), 'Docker and host must select the same packaged daemon bytes');
|
||||
assert.equal(before.codexCommandSha256, codex.codexCommandSha256, 'Docker and host must select the same official Codex command bytes');
|
||||
assert.equal(before.codexNativeExecutableSha256, codex.codexNativeExecutableSha256, 'Docker and host must select the same official native Codex payload bytes');
|
||||
const hostHeader = process.report.getReport().header;
|
||||
assert.match(hostHeader.glibcVersionRuntime || '', /^\d+\.\d+$/, 'Native host startup must report its actual GNU libc ABI');
|
||||
const startup = hostProbe('host-startup-probe.mjs', macInstalledStartupProbeSource(consumer, hostServer, daemon.sha256, join(assets, 'installed-cli.ts')));
|
||||
const after = { lockSha256: digest(readFileSync(join(consumer, 'package-lock.json'))), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: hostCodex().codexCommandSha256 };
|
||||
const afterCodex = hostCodex();
|
||||
const after = { lockSha256: digest(readFileSync(join(consumer, 'package-lock.json'))), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: afterCodex.codexCommandSha256, codexNativeExecutableSha256: afterCodex.codexNativeExecutableSha256 };
|
||||
assertInstalledNativeConsumerContinuity(before, after);
|
||||
assert.equal(startup.runnerSha256, before.runnerSha256);
|
||||
assert.equal(startup.codexSha256, before.codexCommandSha256);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { startLocalBrowserLogin } from "./local-ai-browser-login.js";
|
||||
import { resolveCodexCommand } from "../vendor/paperclip-runner/index.js";
|
||||
|
||||
@@ -42,6 +44,24 @@ function targetPlatform(platform: string, architecture: string, glibc = true, gl
|
||||
if (platform === "linux") vi.spyOn(process.report, "getReport").mockReturnValue({ header: glibc ? { glibcVersionRuntime } : {} } as never);
|
||||
}
|
||||
|
||||
function resolveFixtureCodexCommand(home: string): string {
|
||||
// Vitest's global dependency paths can supply a foreign bridge before the
|
||||
// fixture wrapper. Reuse the existing Codex security test's closed Node seam.
|
||||
const source = `
|
||||
import { resolveCodexCommand } from ${JSON.stringify(new URL("../../../packages/paperclip-runner/src/drivers/codex/codex-command.ts", import.meta.url).href)};
|
||||
try { console.log(JSON.stringify({ command: resolveCodexCommand(${JSON.stringify(path.join(root!, "issuer.js"))}, { PATH: "/missing-codex" }, ${JSON.stringify(home)}) })); }
|
||||
catch (error) { console.log(JSON.stringify({ error: error.message })); }
|
||||
`;
|
||||
const result = JSON.parse(execFileSync(process.execPath, [
|
||||
"--no-global-search-paths", "--import", createRequire(import.meta.url).resolve("tsx"),
|
||||
"--input-type=module", "--eval", source,
|
||||
], { cwd: root, env: { PATH: "/usr/bin:/bin", NODE_PATH: "", NODE_OPTIONS: "" },
|
||||
encoding: "utf8", timeout: 10000, maxBuffer: 16384 }));
|
||||
if (typeof result.error === "string") throw new Error(result.error);
|
||||
if (typeof result.command !== "string") throw new Error("Fixture resolver returned no executable");
|
||||
return result.command;
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === "win32")("local browser subscription login", () => {
|
||||
it.each(["native", "arm32", "musl", "glibc-2.38", "glibc-3.0", "unknown"])("uses the selected Codex command with its credential home and ambient HOME/cwd (%s)", async platform => {
|
||||
const legacy = platform === "arm32";
|
||||
@@ -84,15 +104,18 @@ describe.skipIf(process.platform === "win32")("local browser subscription login"
|
||||
await mkdir(path.join(dependency, "bin"), { recursive: true });
|
||||
await writeFile(path.join(dependency, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.1.0", bin: { codex: "bin/codex" } }));
|
||||
await writeFile(path.join(dependency, "bin/codex"), await readFile(path.join(root!, "bin/codex")), { mode: 0o700 });
|
||||
const actual = await vi.importActual<Pick<typeof import("../vendor/paperclip-runner/index.js"), "resolveCodexCommand">>("../../../packages/paperclip-runner/src/drivers/codex/codex-command.ts");
|
||||
vi.mocked(resolveCodexCommand).mockImplementation(() => actual.resolveCodexCommand(path.join(root!, "issuer.js"), process.env, home));
|
||||
const command = resolveFixtureCodexCommand(home);
|
||||
expect(command).toBe(await realpath(path.join(dependency, "bin/codex")));
|
||||
vi.mocked(resolveCodexCommand).mockReturnValue(command);
|
||||
process.env.PATH = "/usr/bin:/bin";
|
||||
const login = startLocalBrowserLogin("openai", home);
|
||||
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
|
||||
expect(login.outcome).toBe("success");
|
||||
expect(await readFile(path.join(home, "owned-cli"), "utf8")).toBe("owned\n");
|
||||
expect(resolveCodexCommand).toHaveBeenCalledOnce();
|
||||
expect(process.report.getReport).not.toHaveBeenCalled();
|
||||
try {
|
||||
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
|
||||
expect(login.outcome).toBe("success");
|
||||
expect(await readFile(path.join(home, "owned-cli"), "utf8")).toBe("owned\n");
|
||||
expect(resolveCodexCommand).toHaveBeenCalledOnce();
|
||||
expect(process.report.getReport).not.toHaveBeenCalled();
|
||||
} finally { login.abort(); }
|
||||
});
|
||||
|
||||
it.each(["2.38", "musl", "unknown"])("reports a real owned-dependency containment failure on Linux/%s without falling back to another CLI", async libc => {
|
||||
@@ -101,15 +124,20 @@ describe.skipIf(process.platform === "win32")("local browser subscription login"
|
||||
const dependency = path.join(root!, "node_modules/@openai/codex");
|
||||
await mkdir(dependency, { recursive: true });
|
||||
await writeFile(path.join(dependency, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.1.0", bin: { codex: "../outside-codex" } }));
|
||||
const actual = await vi.importActual<Pick<typeof import("../vendor/paperclip-runner/index.js"), "resolveCodexCommand">>("../../../packages/paperclip-runner/src/drivers/codex/codex-command.ts");
|
||||
vi.mocked(resolveCodexCommand).mockImplementation(() => actual.resolveCodexCommand(path.join(root!, "issuer.js"), process.env, home));
|
||||
let resolutionError: unknown;
|
||||
try { resolveFixtureCodexCommand(home); } catch (error) { resolutionError = error; }
|
||||
expect(resolutionError).toBeInstanceOf(Error);
|
||||
expect((resolutionError as Error).message).toContain("executable escapes its package");
|
||||
vi.mocked(resolveCodexCommand).mockImplementation(() => { throw resolutionError; });
|
||||
const login = startLocalBrowserLogin("openai", home);
|
||||
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
|
||||
expect(login.outcome).toBe("failure");
|
||||
expect(login.error).toMatch(/requires an installed Codex CLI.*Install Codex/);
|
||||
expect(JSON.stringify(login)).not.toContain(root!);
|
||||
await expect(readFile(path.join(home, "unreviewed-cli"))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(resolveCodexCommand).toHaveBeenCalledOnce();
|
||||
try {
|
||||
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
|
||||
expect(login.outcome).toBe("failure");
|
||||
expect(login.error).toMatch(/requires an installed Codex CLI.*Install Codex/);
|
||||
expect(JSON.stringify(login)).not.toContain(root!);
|
||||
await expect(readFile(path.join(home, "unreviewed-cli"))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
expect(resolveCodexCommand).toHaveBeenCalledOnce();
|
||||
} finally { login.abort(); }
|
||||
});
|
||||
|
||||
it.each([["linux", "x64"], ["linux", "arm64"], ["darwin", "arm64"], ["darwin", "x64"]] as const)("reports an unavailable Codex CLI on %s/%s without revealing private details", async (platform, architecture) => {
|
||||
|
||||
@@ -48,6 +48,7 @@ test("project creation and repository configuration persist on a short mobile vi
|
||||
await section.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await section.getByRole("textbox", { name: "Existing repo URL" }).fill("https://git.example/org/updated");
|
||||
await section.getByRole("button", { name: "Save URL" }).click();
|
||||
await expect(section.getByText("https://git.example/org/updated", { exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(section.getByText("https://git.example/org/updated", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Set the KEY to the env var name", { exact: false })).toHaveCount(0);
|
||||
|
||||
Reference in new issue
Block a user