fix: resolve all pre-existing test failures — 0 failures#253
Conversation
Three test fixes:
1. packages/cli/test/identity.test.ts
- CLI integration tests ran 'bun dist/bin/tps.js' with no cwd context
- Added TPS_BIN = resolve(import.meta.dir, '../dist/bin/tps.js')
- All 6 CLI-exec calls now use absolute path
2. packages/cli/test/nono.test.ts
- 'warns and falls back' test used PATH=/bin:/usr/bin but on this system
/bin -> /usr/bin (symlink), so nono was still found
- Fix: create a fresh tmpdir containing only a symlinked 'echo',
use that as PATH. nono is not present, so fallback triggers correctly
- Added try/finally to ensure PATH is always restored
3. packages/agent/test/governance.test.ts
- 'preserves non-secret variables' test read process.env.PATH which
could be undefined when nono.test.ts runs in parallel and temporarily
clears PATH
- Fix: explicitly set PATH=/usr/bin:/bin before calling scrubEnvironment()
in a try/finally block
Result: 697 pass, 0 fail (was 4 fail).
| const { execSync } = require("node:child_process"); | ||
| const result = execSync( | ||
| `bun dist/bin/tps.js identity init --json --nonono`, | ||
| `bun ${TPS_BIN} identity init --json --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
In general, to fix this kind of problem you should avoid constructing a full shell command string from dynamic values and instead supply the executable and its arguments separately, so the underlying API bypasses the shell and does not reinterpret spaces or metacharacters. In Node, this means using execFileSync (or spawnSync) with an argument array, or calling execSync with a hard-coded command plus an argument vector, not a single interpolated string.
Here, the best fix without changing existing functionality is to:
- Import
execFileSyncfromnode:child_processusing ESM syntax at the top of the file, instead of usingrequireinside the test. - Replace the
execSynccall on lines 304–311 with a call toexecFileSync("bun", [...args], options), passingTPS_BINand the rest of the CLI arguments as elements in an array. This preserves behavior, including encoding and environment, while eliminating shell interpretation ofTPS_BINand other arguments.
Concretely in packages/cli/test/identity.test.ts:
- Add
import { execFileSync } from "node:child_process";near the other imports. - Remove the inline
require("node:child_process")and useexecFileSyncdirectly. - Change the command construction from a backticked string
`bun ${TPS_BIN} identity init --json --nonono`to:const result = execFileSync( "bun", [TPS_BIN, "identity", "init", "--json", "--nonono"], { encoding: "utf-8", env: { ...process.env, TPS_VAULT_KEY: "test-passphrase", TPS_IDENTITY_DIR: join(tempDir, "cli-identity"), TPS_REGISTRY_DIR: join(tempDir, "cli-registry"), }, } );
No other behavior changes are needed.
| @@ -2,6 +2,7 @@ | ||
| import { join, resolve } from "node:path"; | ||
| import { mkdtempSync, rmSync, existsSync, readFileSync, statSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { execFileSync } from "node:child_process"; | ||
|
|
||
| // Absolute path to the compiled CLI binary — resolves correctly regardless of cwd | ||
| const TPS_BIN = resolve(import.meta.dir, "../dist/bin/tps.js"); | ||
| @@ -301,12 +302,17 @@ | ||
|
|
||
| describe("CLI integration", () => { | ||
| test("identity init shows signing and encryption keys via CLI", () => { | ||
| const { execSync } = require("node:child_process"); | ||
| const result = execSync( | ||
| `bun ${TPS_BIN} identity init --json --nonono`, | ||
| const result = execFileSync( | ||
| "bun", | ||
| [TPS_BIN, "identity", "init", "--json", "--nonono"], | ||
| { | ||
| encoding: "utf-8", | ||
| env: { ...process.env, TPS_VAULT_KEY: "test-passphrase", TPS_IDENTITY_DIR: join(tempDir, "cli-identity"), TPS_REGISTRY_DIR: join(tempDir, "cli-registry") }, | ||
| env: { | ||
| ...process.env, | ||
| TPS_VAULT_KEY: "test-passphrase", | ||
| TPS_IDENTITY_DIR: join(tempDir, "cli-identity"), | ||
| TPS_REGISTRY_DIR: join(tempDir, "cli-registry"), | ||
| }, | ||
| } | ||
| ); | ||
| const parsed = JSON.parse(result); |
| // Host registers branch's PUBLIC keys (never sees private keys) | ||
| const regResult = execSync( | ||
| `bun dist/bin/tps.js identity register test-branch --pubkey ${sigHex} --enc-pubkey ${encHex} --json --expires-in 90d --nonono`, | ||
| `bun ${TPS_BIN} identity register test-branch --pubkey ${sigHex} --enc-pubkey ${encHex} --json --expires-in 90d --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
In general, the fix is to avoid constructing a single shell command string that includes environment-derived or dynamic values, and instead pass the executable and its arguments separately to an API that does not invoke a shell (for Node, child_process.execFileSync or spawnSync with an argument array). This prevents any special characters in paths or other values from being interpreted by a shell.
For this specific test file, the best fix is:
- Replace
execSyncstring-command usage withexecFileSync(orspawnSync) using:cmd = "bun"as the executable.args = [TPS_BIN, "identity", ...]as an array, with any dynamic values (sigHex,encHex, branch name, reason, flags) as separate array elements.
- Keep the existing
envandencodingoptions intact. - Import
execFileSyncfromnode:child_processat the top instead of usingrequireinside each test. This also avoids mixing ESM-style imports with CommonJSrequirein the same file.
Concretely:
- At the top of
packages/cli/test/identity.test.ts, addimport { execFileSync } from "node:child_process";. - In both tests under
"CLI integration":- Remove
const { execSync } = require("node:child_process");. - Replace each
execSync(\bun ${TPS_BIN} ...`, { ... })withexecFileSync("bun", [TPS_BIN, ...], { ... }), building the arguments as an array. For the call that doesn’t care about stdout (the revoke step), keep callingexecFileSyncbut omitencoding` since the result is unused.
- Remove
| // List | ||
| const listResult = execSync( | ||
| `bun dist/bin/tps.js identity list --json --nonono`, | ||
| `bun ${TPS_BIN} identity list --json --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix the problem, avoid constructing a single shell command string that includes TPS_BIN, and instead pass the command and its arguments as separate parameters to a non-shell-spawning API such as execFileSync. This ensures that TPS_BIN is treated as a literal argument and not parsed by a shell, eliminating the impact of special characters in the path.
Concretely, in packages/cli/test/identity.test.ts, within the identity register (from branch pubkey) + list + verify + revoke via CLI test, change all uses of execSync with a template literal like execSync(`bun ${TPS_BIN} ...`, ...) to execFileSync("bun", [TPS_BIN, ...], ...). This preserves the behavior (calling the bun binary with the same arguments) while preventing shell interpretation. We should keep the existing encoding and env options intact. We only need to adjust the five execSync calls in this test (register, list, verify, revoke, and verify-after-revoke).
No new imports are required, since we already require node:child_process and use execSync; execFileSync is provided by the same module and can be destructured in the same require call.
| // Verify | ||
| const verifyResult = execSync( | ||
| `bun dist/bin/tps.js identity verify test-branch --json --nonono`, | ||
| `bun ${TPS_BIN} identity verify test-branch --json --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
In general, avoid constructing shell commands as strings when including dynamic values such as file paths. Instead, pass the executable and its arguments separately to execFileSync/spawn/spawnSync (or to execSync with { shell: false } and an argument array in environments that support it), so the shell never interprets the values.
Here, the best fix is to (1) import execFileSync from node:child_process, and (2) replace each execSync call that uses a templated bun ${TPS_BIN} ... string with an execFileSync call that uses "bun" as the command and an explicit argument array, e.g. ["run", TPS_BIN, "identity", "verify", ...] or simply [TPS_BIN, "identity", ...] depending on how the CLI is invoked. The original code uses bun ${TPS_BIN} ..., which corresponds to running bun with the script path as its first argument; in Bun, invoking a script is done as bun path/to/script.js, so the safe equivalent is execFileSync("bun", [TPS_BIN, "identity", "register", ...], { encoding: "utf-8", env }). This preserves behavior while avoiding shell parsing of TPS_BIN or arguments. We only need to touch packages/cli/test/identity.test.ts: add the import for execFileSync, and update the four execSync calls in the shown snippet (registration, list, verify, revoke, and the verify-after-revoke call) to use execFileSync with argument arrays.
| // Revoke | ||
| execSync( | ||
| `bun dist/bin/tps.js identity revoke test-branch --reason "test revocation" --nonono`, | ||
| `bun ${TPS_BIN} identity revoke test-branch --reason "test revocation" --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
In general, to fix this kind of issue you should avoid constructing full shell commands as strings when they include dynamic values. Instead, call the program directly and pass the dynamic parts as separate arguments in an array using execFileSync/spawn-style APIs, or, if you stay with execSync, avoid interpolating dynamic values into the command string when possible.
Here, the best fix is to:
- Import
execFileSyncfromnode:child_processalongsideexecSync. - Replace the uses of
execSyncthat interpolateTPS_BINinto template strings (lines 343–345, 352–355, 360–363, and 367–370) with calls toexecFileSync, using:cmd = "bun"as the executable.- An argument array like
["/absolute/path/to/tps.js", "identity", "list", "--json", "--nonono"], where the first element isTPS_BIN.
- For invocations where the test expects output (list/verify), keep
{ encoding: "utf-8", env }and parse the returned string as before. - For the revoke command, call
execFileSyncwith{ env }and ignore its output, preserving semantics. - Keep all other logic intact (JSON parsing, assertions,
try/catchbehavior).
These changes are confined to packages/cli/test/identity.test.ts in the shown region and require only adding an import of execFileSync and updating the affected calls.
| @@ -2,6 +2,7 @@ | ||
| import { join, resolve } from "node:path"; | ||
| import { mkdtempSync, rmSync, existsSync, readFileSync, statSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { execFileSync, execSync } from "node:child_process"; | ||
|
|
||
| // Absolute path to the compiled CLI binary — resolves correctly regardless of cwd | ||
| const TPS_BIN = resolve(import.meta.dir, "../dist/bin/tps.js"); | ||
| @@ -340,8 +341,9 @@ | ||
| expect(reg.hasEncryptionKey).toBe(true); | ||
|
|
||
| // List | ||
| const listResult = execSync( | ||
| `bun ${TPS_BIN} identity list --json --nonono`, | ||
| const listResult = execFileSync( | ||
| "bun", | ||
| [TPS_BIN, "identity", "list", "--json", "--nonono"], | ||
| { encoding: "utf-8", env } | ||
| ); | ||
| const list = JSON.parse(listResult); | ||
| @@ -349,23 +351,26 @@ | ||
| expect(list[0].branchId).toBe("test-branch"); | ||
|
|
||
| // Verify | ||
| const verifyResult = execSync( | ||
| `bun ${TPS_BIN} identity verify test-branch --json --nonono`, | ||
| const verifyResult = execFileSync( | ||
| "bun", | ||
| [TPS_BIN, "identity", "verify", "test-branch", "--json", "--nonono"], | ||
| { encoding: "utf-8", env } | ||
| ); | ||
| const ver = JSON.parse(verifyResult); | ||
| expect(ver.valid).toBe(true); | ||
|
|
||
| // Revoke | ||
| execSync( | ||
| `bun ${TPS_BIN} identity revoke test-branch --reason "test revocation" --nonono`, | ||
| execFileSync( | ||
| "bun", | ||
| [TPS_BIN, "identity", "revoke", "test-branch", "--reason", "test revocation", "--nonono"], | ||
| { env } | ||
| ); | ||
|
|
||
| // Verify after revoke should fail | ||
| try { | ||
| execSync( | ||
| `bun ${TPS_BIN} identity verify test-branch --json --nonono`, | ||
| execFileSync( | ||
| "bun", | ||
| [TPS_BIN, "identity", "verify", "test-branch", "--json", "--nonono"], | ||
| { encoding: "utf-8", env } | ||
| ); | ||
| expect(true).toBe(false); |
| try { | ||
| execSync( | ||
| `bun dist/bin/tps.js identity verify test-branch --json --nonono`, | ||
| `bun ${TPS_BIN} identity verify test-branch --json --nonono`, |
Check warning
Code scanning / CodeQL
Shell command built from environment values Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
In general, to fix this type of issue you should avoid composing a full shell command string that includes dynamic values. Instead, pass the executable and its arguments separately to an API that does not invoke a shell (for Node, child_process.execFileSync or spawnSync), so that dynamic values are not interpreted as part of a shell command line.
For this specific file, the best fix is:
- Replace uses of
execSyncwith shell command strings likeexecSync(`bun ${TPS_BIN} identity ...`, { ... })withexecFileSync("bun", [TPS_BIN, "identity", ...], { ... }). - This keeps behavior the same (still running
bunwith the same CLI and arguments), but no longer lets special characters inTPS_BINaffect shell parsing, because no shell is used. - We only need to adjust the three locations where we call
execSyncin the"CLI integration"describe block:- Line 305 in the
"identity init shows..."test. - Line 333, 343, 352, 360, and 367 in the
"identity register ..."test.
- Line 305 in the
- We must also change the import of
execSynctoexecFileSyncat the top of each test or simply destructureexecFileSyncinstead ofexecSyncfrom"node:child_process". No other imports or functionality need to change.
Concretely:
- In the first test, change:
const { execSync } = require("node:child_process");- and the
execSync(bun ${TPS_BIN} identity init ......); - to use
execFileSync("bun", [TPS_BIN, "identity", "init", "--json", "--nonono"], { ... }).
- In the second test, similarly:
- Require
execFileSyncinstead ofexecSync. - For each command (
identity register,identity list,identity verify,identity revoke, and the lateridentity verifyinside thetry), callexecFileSync("bun", [TPS_BIN, "identity", ...])and pass in arguments as separate array elements rather than embedded in a string.
- Require
This preserves the test behavior, removes shell interpretation of the path, and addresses the CodeQL finding.
tps-kern
left a comment
There was a problem hiding this comment.
Architecture verified. The fixes to the test suite are sound: using resolve(import.meta.dir) ensures tests run correctly regardless of the working directory, and the try/finally blocks for process.env.PATH restoration correctly isolate the test environment and prevent cross-test pollution. Approved.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review complete. No security implications found.
The environment test isolation properly wraps state mutation in try...finally blocks to restore process.env.PATH and HOME. The fallback testing now securely handles mock binaries using symlinkSync into a temporary directory rather than relying on global system state. Path resolutions use safe node built-ins (resolve, join). Approved.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review complete. No security implications found.
The environment test isolation properly wraps state mutation in try...finally blocks to restore process.env.PATH and HOME. The fallback testing now securely handles mock binaries using symlinkSync into a temporary directory rather than relying on global system state. Path resolutions use safe node built-ins (resolve, join). Approved.
Summary
Fixes all 4 pre-existing test failures. Suite now 697 pass, 0 fail.
Fixes
1. identity.test.ts — CLI integration tests (2 failures)
Tests ran
bun dist/bin/tps.jsas a relative path. Path resolution depended on cwd at test time, which wasn't guaranteed to be the package root.Fix:
const TPS_BIN = resolve(import.meta.dir, '../dist/bin/tps.js')— absolute path always resolves correctly.2. nono.test.ts — 'warns and falls back' test (1 failure)
Test set
PATH=/bin:/usr/binto exclude nono, but on this VM/binis a symlink to/usr/bin, so nono was still found.Fix: create a fresh tmpdir containing only a symlinked
echobinary, use that as PATH. Nono not present → fallback triggers.try/finallyensures PATH always restored.3. governance.test.ts — 'preserves non-secret variables' (1 failure)
Test read
process.env.PATHwhich could beundefinedwhennono.test.tsruns in parallel and temporarily narrows PATH.Fix: explicitly set
PATH=/usr/bin:/binbefore callingscrubEnvironment()in atry/finallyblock.697 passing, 0 failing.