mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 17:56:14 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fb01dae77 | ||
|
|
f90fd8567e | ||
|
|
77960229b8 | ||
|
|
60dc84e7a3 |
No files matched your search
@@ -1,27 +1,71 @@
|
||||
---
|
||||
description: Project guidelines for the open-source CLI coding agent (zero) repository.
|
||||
globs: "*.go, *.js, *.md, *.json, *.toml, *.yaml, *.yml"
|
||||
alwaysApply: false
|
||||
---
|
||||
# Repository Guidelines for Zero
|
||||
|
||||
# Repository Conventions for Zero
|
||||
These instructions apply to all work in this repository. For the user-facing
|
||||
guide to extending Zero with specialists, hooks, plugins, MCP, and skills, see
|
||||
[docs/EXTENDING.md](docs/EXTENDING.md).
|
||||
|
||||
This file outlines the project conventions and repository guidelines for coding agents when working on the `zero` repository. For the general guide on how to extend Zero (write specialist sub-agents, hooks, plugins, MCP, skills), see [docs/EXTENDING.md](docs/EXTENDING.md).
|
||||
## 1. Contribution and Pull Request Rules
|
||||
|
||||
## 1. Project Conventions
|
||||
- Before opening any pull request, **all contributors**—including maintainers,
|
||||
community contributors, and coding agents—must read and follow
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- Community pull requests require an existing parent issue with the
|
||||
`issue-approved` label. Team members may open pull requests through the
|
||||
internal development process described in `CONTRIBUTING.md`.
|
||||
- Keep each change focused on the approved or assigned scope. Do not include
|
||||
unrelated fixes, refactors, formatting churn, generated output, or existing
|
||||
local changes in the same commit or pull request.
|
||||
- Discuss new implementation languages, runtimes, major dependency changes,
|
||||
and broad architectural rewrites with maintainers before implementation.
|
||||
- Pull request descriptions must explain what changed and why, link the parent
|
||||
issue when required, and list the tests or verification performed. Include
|
||||
screenshots or a short recording for user-visible UI changes when practical.
|
||||
|
||||
- Build with `make`, not `go build` directly.
|
||||
- Tests live next to the source file (`foo_test.go` next to `foo.go`).
|
||||
- Run `make lint` before opening a PR.
|
||||
- Never edit files under `third_party/` — those are vendored.
|
||||
- Unify functions/methods where possible to prevent codebase inflation. Prefer a single cross-platform function with conditional checks over duplicating helpers per platform.
|
||||
## 2. Repository and Implementation Conventions
|
||||
|
||||
## 2. Guidelines for Coding Agents
|
||||
- Use the Go version declared in `go.mod`. Do not hardcode a different local
|
||||
toolchain version in scripts or documentation.
|
||||
- Use the repository build and release commands (`make` and
|
||||
`go run ./cmd/zero-release ...`) instead of inventing parallel build flows.
|
||||
- Keep tests beside their source files (`foo_test.go` next to `foo.go`). Add a
|
||||
regression test for behavior changes and run affected concurrent code under
|
||||
the race detector.
|
||||
- Never edit files under `third_party/`; they are vendored.
|
||||
- Prefer one cross-platform function with small conditional checks over
|
||||
duplicated platform-specific helpers when the behavior can remain unified.
|
||||
- Do not commit generated benchmark reports from
|
||||
`internal/perfbench/reports/*.json`; reports are configuration-specific
|
||||
evidence, not repository state.
|
||||
- Preserve the user's working tree. Do not overwrite, delete, stage, or commit
|
||||
unrelated tracked or untracked files.
|
||||
|
||||
If you are an AI coding agent executing tasks in this repository, you **MUST** run all Go code quality and security checks before committing code or completing your task:
|
||||
## 3. Required Validation
|
||||
|
||||
1. **Format & Vet**: Run `go fmt ./...` and `go vet ./...`.
|
||||
2. **Lint**: Run `go run github.com/golangci/golangci-lint/v2/cmd/[email protected] run --enable-only unused,ineffassign,staticcheck ./...`.
|
||||
3. **Security**: Run `go run golang.org/x/vuln/cmd/[email protected] ./...`.
|
||||
Run validation from the repository root before committing, opening a pull
|
||||
request, or completing an implementation task:
|
||||
|
||||
If any check fails or cannot be run, do not ignore it. Prompt the user for instructions or setup assistance before proceeding.
|
||||
1. **Formatting check**: `make fmt-check`. If it fails, format with
|
||||
`go fmt ./...` (or `make fmt`) and run the check again.
|
||||
2. **Vet**: `go vet ./...` (or `make vet`).
|
||||
3. **Tests**: `go test ./...`. Use `make test` for the full race-enabled suite,
|
||||
or run focused tests with `-race`, when concurrency is affected.
|
||||
4. **Build**: `go run ./cmd/zero-release build`.
|
||||
5. **Smoke test**: `go run ./cmd/zero-release smoke`.
|
||||
6. **Advisory lint**:
|
||||
`go run github.com/golangci/golangci-lint/v2/cmd/[email protected] run --enable-only unused,ineffassign,staticcheck ./...`.
|
||||
7. **Security**:
|
||||
`go run golang.org/x/vuln/cmd/[email protected] ./...`.
|
||||
8. **Diff hygiene**: `git diff HEAD --check` (covers staged and unstaged
|
||||
tracked changes).
|
||||
|
||||
`make lint` currently runs the formatting check and `go vet`; it does **not**
|
||||
run golangci-lint. The pinned golangci-lint job is advisory in CI while the
|
||||
existing repository-wide backlog is cleaned up. Fix findings introduced by or
|
||||
related to the current change. Unrelated pre-existing advisory findings do not
|
||||
justify expanding a focused pull request; report them separately.
|
||||
|
||||
Formatting, vet, tests, build, smoke, diff hygiene, and govulncheck are hard
|
||||
requirements. If a related check fails, fix it. If a required check cannot run
|
||||
because of the environment or fails for an unrelated external reason, report
|
||||
the exact failure and obtain maintainer direction rather than silently ignoring
|
||||
it.
|
||||
+159
-15
@@ -55,6 +55,133 @@ function localControlHelperManifest(packageRoot) {
|
||||
return JSON.stringify({ version: 1, helpers });
|
||||
}
|
||||
|
||||
// Mirrors internal/cli/observability.go parseDoctorArgs + writeDoctorHelp so the
|
||||
// wrapper's missing-binary doctor fallback matches the Go doctor CLI surface.
|
||||
const DOCTOR_HELP = `Usage:
|
||||
zero doctor [flags]
|
||||
|
||||
Runs Go backend health checks for config and provider setup.
|
||||
|
||||
Flags:
|
||||
--json Print JSON report
|
||||
--connectivity Include provider endpoint connectivity probe when available
|
||||
-h, --help Show this help
|
||||
`;
|
||||
|
||||
const EXIT_USAGE = 2;
|
||||
|
||||
function installedByBun() {
|
||||
if (process.env.ZERO_WRAPPER_SIMULATE_BUN === '1') {
|
||||
return true;
|
||||
}
|
||||
return process.execPath.includes('bun') || !!process.versions?.bun;
|
||||
}
|
||||
|
||||
function bunRecoveryParagraph() {
|
||||
return (
|
||||
'You installed with Bun, which does not run dependency lifecycle scripts\n' +
|
||||
'by default. Trust the package to run the blocked postinstall:\n' +
|
||||
' bun pm trust @gitlawb/zero (project install)\n' +
|
||||
' bun pm -g trust @gitlawb/zero (global install)\n' +
|
||||
'On Bun versions without `bun pm trust`, add\n' +
|
||||
' "trustedDependencies": ["@gitlawb/zero"]\n' +
|
||||
'to your project package.json and reinstall.\n' +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
function buildFromSourceParagraph() {
|
||||
return (
|
||||
'If this platform has no prebuilt binary, build from source:\n' +
|
||||
'https://github.com/Gitlawb/zero (go run ./cmd/zero, requires Go 1.26+).'
|
||||
);
|
||||
}
|
||||
|
||||
function missingBinaryContextParagraph() {
|
||||
return (
|
||||
'Normally npm installs it as an optional dependency of @gitlawb/zero\n' +
|
||||
`(@gitlawb/zero-${process.platform}-${process.arch}), and the wrapper can\n` +
|
||||
'also download it from the GitHub Release when it is missing.'
|
||||
);
|
||||
}
|
||||
|
||||
function missingNativeRecoveryParagraphs(postinstallScript) {
|
||||
const ranByBun = installedByBun();
|
||||
return (
|
||||
missingBinaryContextParagraph() +
|
||||
'\n' +
|
||||
'\n' +
|
||||
'Things to try:\n' +
|
||||
' - reinstall without omitting optional dependencies:\n' +
|
||||
' npm install -g @gitlawb/zero\n' +
|
||||
' - run the downloader manually (needs write access to the package dir):\n' +
|
||||
` node "${postinstallScript}"\n` +
|
||||
'\n' +
|
||||
(ranByBun ? bunRecoveryParagraph() : '') +
|
||||
buildFromSourceParagraph()
|
||||
);
|
||||
}
|
||||
|
||||
function formatGenericMissingNativeBinaryMessage(postinstallScript) {
|
||||
return (
|
||||
'[zero] No native binary is available for this install.\n' +
|
||||
missingNativeRecoveryParagraphs(postinstallScript)
|
||||
);
|
||||
}
|
||||
|
||||
function parseDoctorArgs(args) {
|
||||
let json = false;
|
||||
for (const arg of args) {
|
||||
switch (arg) {
|
||||
case '-h':
|
||||
case '--help':
|
||||
case 'help':
|
||||
return { kind: 'help' };
|
||||
case '--json':
|
||||
json = true;
|
||||
break;
|
||||
case '--connectivity':
|
||||
break;
|
||||
default:
|
||||
return { kind: 'error', message: `unknown doctor flag ${JSON.stringify(arg)}` };
|
||||
}
|
||||
}
|
||||
return { kind: 'run', json };
|
||||
}
|
||||
|
||||
function missingNativeDoctorJSONReport(postinstallScript) {
|
||||
return {
|
||||
generatedAt: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
||||
ok: false,
|
||||
checks: [
|
||||
{
|
||||
id: 'runtime.go',
|
||||
label: 'Go runtime',
|
||||
status: 'fail',
|
||||
message: 'Native zero binary is missing next to the npm wrapper.',
|
||||
details: {
|
||||
remedy: `node "${postinstallScript}"`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function missingNativeDoctorTextReport(postinstallScript) {
|
||||
return (
|
||||
'Zero doctor report (' +
|
||||
new Date().toISOString() +
|
||||
')\n' +
|
||||
'Overall: fail\n' +
|
||||
'[fail] runtime.go - Native zero binary is missing next to the npm wrapper.\n' +
|
||||
' remedy: node "' +
|
||||
postinstallScript +
|
||||
'"\n' +
|
||||
'\n' +
|
||||
missingNativeRecoveryParagraphs(postinstallScript)
|
||||
);
|
||||
}
|
||||
|
||||
// The platform payload is a version of @gitlawb/zero itself, installed under
|
||||
// an npm: alias (see docs/NPM_PACKAGING.md). The alias name is derived from
|
||||
// process.platform/process.arch, so an unsupported platform simply fails to
|
||||
@@ -114,21 +241,38 @@ function resolveNativeBinary() {
|
||||
const nativePath = resolveNativeBinary();
|
||||
|
||||
if (!nativePath) {
|
||||
console.error(
|
||||
'[zero] No native binary is available for this install.\n' +
|
||||
'Normally npm installs it as an optional dependency of @gitlawb/zero\n' +
|
||||
`(@gitlawb/zero-${process.platform}-${process.arch}), and the wrapper can\n` +
|
||||
'also download it from the GitHub Release when it is missing.\n' +
|
||||
'\n' +
|
||||
'Things to try:\n' +
|
||||
' - reinstall without omitting optional dependencies:\n' +
|
||||
' npm install -g @gitlawb/zero\n' +
|
||||
' - run the downloader manually (needs write access to the package dir):\n' +
|
||||
` node "${join(packageRoot, 'scripts', 'postinstall.mjs')}"\n` +
|
||||
'\n' +
|
||||
'If this platform has no prebuilt binary, build from source:\n' +
|
||||
'https://github.com/Gitlawb/zero (go run ./cmd/zero, requires Go 1.26+).',
|
||||
);
|
||||
const postinstallScript = join(packageRoot, 'scripts', 'postinstall.mjs');
|
||||
|
||||
// `zero doctor` is the diagnostic command: when the native binary is missing
|
||||
// it's the one invocation that must NOT bail with the generic wrapper error,
|
||||
// because that's exactly the blind alley issue #405 reports. Instead, surface
|
||||
// a doctor-shaped FAIL line for the runtime so the user's diagnostic finds the
|
||||
// real cause. We branch on a literal 'doctor' subcommand only (matching `zero
|
||||
// doctor` and `zero doctor --connectivity`), preserving the existing bail for
|
||||
// every other invocation (exec, providers list, TUI, --version, etc.).
|
||||
const argv = process.argv.slice(2);
|
||||
const isDoctor = argv.length > 0 && argv[0] === 'doctor';
|
||||
if (isDoctor) {
|
||||
const parsed = parseDoctorArgs(argv.slice(1));
|
||||
if (parsed.kind === 'help') {
|
||||
process.stdout.write(DOCTOR_HELP);
|
||||
process.exit(0);
|
||||
}
|
||||
if (parsed.kind === 'error') {
|
||||
process.stderr.write(`[zero] ${parsed.message}\n`);
|
||||
process.exit(EXIT_USAGE);
|
||||
}
|
||||
|
||||
if (parsed.json) {
|
||||
process.stdout.write(JSON.stringify(missingNativeDoctorJSONReport(postinstallScript), null, 2) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(missingNativeDoctorTextReport(postinstallScript) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.error(formatGenericMissingNativeBinaryMessage(postinstallScript));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,54 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mirror internal/cli/exec.go exit codes so wrapper doctor fallback tests assert
|
||||
// the same CLI contract as the Go doctor command.
|
||||
const (
|
||||
wrapperExitSuccess = 0
|
||||
wrapperExitUsage = 2
|
||||
wrapperExitDoctor = 1
|
||||
)
|
||||
|
||||
// Canonical Bun recovery copy from bin/zero.js bunRecoveryParagraph() — shared
|
||||
// by the generic missing-binary path and the doctor text fallback.
|
||||
const (
|
||||
bunRecoveryLead = "You installed with Bun, which does not run dependency lifecycle scripts"
|
||||
bunPmTrustProject = "bun pm trust @gitlawb/zero"
|
||||
bunPmTrustGlobal = "bun pm -g trust @gitlawb/zero"
|
||||
bunRecoveryTrustedDeps = `"trustedDependencies": ["@gitlawb/zero"]`
|
||||
buildFromSourceLead = "If this platform has no prebuilt binary, build from source:"
|
||||
)
|
||||
|
||||
func runWrapperFixture(t *testing.T, wrapperPath string, args ...string) (stdout string, stderr string, exitCode int) {
|
||||
return runWrapperFixtureWithEnv(t, wrapperPath, nil, args...)
|
||||
}
|
||||
|
||||
func runWrapperFixtureWithEnv(t *testing.T, wrapperPath string, extraEnv []string, args ...string) (stdout string, stderr string, exitCode int) {
|
||||
t.Helper()
|
||||
node := requireNode(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nodeWrapperTimeout())
|
||||
defer cancel()
|
||||
command := nodeWrapperCommand(ctx, node, wrapperPath, args...)
|
||||
command.Env = append(withoutEnvKey(command.Env, "ZERO_LOCAL_CONTROL_HELPERS"), "ZERO_LOCAL_CONTROL_HELPERS=")
|
||||
command.Env = append(withoutEnvKey(command.Env, "ZERO_WRAPPER_SIMULATE_BUN"), extraEnv...)
|
||||
var stdoutBuf, stderrBuf strings.Builder
|
||||
command.Stdout = &stdoutBuf
|
||||
command.Stderr = &stderrBuf
|
||||
err := command.Run()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("wrapper timed out: %v; stdout: %s stderr: %s", ctx.Err(), stdoutBuf.String(), stderrBuf.String())
|
||||
}
|
||||
exitCode = wrapperExitSuccess
|
||||
if err != nil {
|
||||
exitErr, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("wrapper err = %v, want *exec.ExitError; stdout=%s stderr=%s", err, stdoutBuf.String(), stderrBuf.String())
|
||||
}
|
||||
exitCode = exitErr.ExitCode()
|
||||
}
|
||||
return stdoutBuf.String(), stderrBuf.String(), exitCode
|
||||
}
|
||||
|
||||
func TestPackageBinPointsToNodeWrapper(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
bytes, err := os.ReadFile(filepath.Join(root, "package.json"))
|
||||
@@ -393,6 +441,221 @@ func nodeArchName() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #405: `zero doctor` is the diagnostic command, so when the native
|
||||
// binary is the thing that's broken it must NOT bail with the generic wrapper
|
||||
// error; that's exactly the blind alley the bug report calls out. Instead it
|
||||
// emits a doctor-shaped FAIL line for the runtime so the user's own diagnostic
|
||||
// surfaces the real cause. We assert on the doctor-report shape (so the doctor
|
||||
// UX matches what the Go-side doctor.Format produces) and on exit 1 (a missing
|
||||
// binary is still a hard failure, not a pass).
|
||||
func TestNodeWrapperDoctorReportsMissingNativeBinaryAsDoctorFail(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, "doctor")
|
||||
if exitCode != wrapperExitDoctor {
|
||||
t.Fatalf("doctor exit = %d, want %d; stdout=%s stderr=%s", exitCode, wrapperExitDoctor, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("doctor text report must go to stdout only, got stderr=%q", stderr)
|
||||
}
|
||||
// Doctor-shaped report, not the generic wrapper bail. Matches the shape the
|
||||
// Go-side doctor.Format emits: a header, "Overall: <pass/fail>", then
|
||||
// "[<status>] <id> - <message>" lines.
|
||||
if !strings.Contains(stdout, "Zero doctor report (") {
|
||||
t.Fatalf("doctor output should start with a doctor report header, got stdout=%q", stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "Overall: fail") {
|
||||
t.Fatalf("doctor overall must be fail when the native binary is missing, got stdout=%q", stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "[fail] runtime.go") {
|
||||
t.Fatalf("doctor must report a failing runtime.go check, got stdout=%q", stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "Native zero binary is missing next to the npm wrapper") {
|
||||
t.Fatalf("doctor must name the actual cause (missing native binary), got stdout=%q", stdout)
|
||||
}
|
||||
// The actionable remedy must point at the postinstall script that would fix
|
||||
// the install, not just "build from source".
|
||||
if !strings.Contains(stdout, "postinstall.mjs") {
|
||||
t.Fatalf("doctor remedy should name the postinstall script, got stdout=%q", stdout)
|
||||
}
|
||||
// Regression guard for the original blind-alley bug: the doctor path must
|
||||
// NOT emit the generic wrapper bail (that's what sent users debugging the
|
||||
// wrong thing).
|
||||
if strings.Contains(stdout, "[zero] No native binary is available for this install") {
|
||||
t.Fatalf("doctor must not emit the generic wrapper bail, got stdout=%q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperDoctorJSONReportsMissingNativeBinaryAsJSONFail(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, "doctor", "--json")
|
||||
if exitCode != wrapperExitDoctor {
|
||||
t.Fatalf("doctor --json exit = %d, want %d; stdout=%s stderr=%s", exitCode, wrapperExitDoctor, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("doctor --json should write machine-readable output to stdout only, got stderr=%q", stderr)
|
||||
}
|
||||
var report struct {
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
OK bool `json:"ok"`
|
||||
Checks []struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]any `json:"details"`
|
||||
} `json:"checks"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(stdout), &report); err != nil {
|
||||
t.Fatalf("doctor --json stdout should be valid JSON, got %q: %v", stdout, err)
|
||||
}
|
||||
if report.GeneratedAt == "" {
|
||||
t.Fatalf("doctor --json report should include generatedAt: %#v", report)
|
||||
}
|
||||
if report.OK {
|
||||
t.Fatalf("doctor --json ok = true, want false: %#v", report)
|
||||
}
|
||||
if len(report.Checks) != 1 {
|
||||
t.Fatalf("doctor --json checks length = %d, want 1: %#v", len(report.Checks), report.Checks)
|
||||
}
|
||||
check := report.Checks[0]
|
||||
if check.ID != "runtime.go" || check.Label != "Go runtime" || check.Status != "fail" {
|
||||
t.Fatalf("doctor --json runtime check = %#v, want failing runtime.go check", check)
|
||||
}
|
||||
if !strings.Contains(check.Message, "Native zero binary is missing next to the npm wrapper") {
|
||||
t.Fatalf("doctor --json must name the actual cause, got %#v", check)
|
||||
}
|
||||
remedy, _ := check.Details["remedy"].(string)
|
||||
if !strings.Contains(remedy, "postinstall.mjs") {
|
||||
t.Fatalf("doctor --json remedy should name the postinstall script, got %#v", check.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperDoctorHelpShowsUsage(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
for _, args := range [][]string{{"doctor", "--help"}, {"doctor", "help"}, {"doctor", "-h"}} {
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, args...)
|
||||
if exitCode != wrapperExitSuccess {
|
||||
t.Fatalf("%v exit = %d, want %d; stdout=%s stderr=%s", args, exitCode, wrapperExitSuccess, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("%v help must write to stdout only, got stderr=%q", args, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "Usage:") || !strings.Contains(stdout, "zero doctor [flags]") {
|
||||
t.Fatalf("%v help output = %q, want doctor usage text", args, stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperDoctorRejectsUnknownFlag(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, "doctor", "--bogus")
|
||||
if exitCode != wrapperExitUsage {
|
||||
t.Fatalf("doctor --bogus exit = %d, want %d; stdout=%s stderr=%s", exitCode, wrapperExitUsage, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stdout) != "" {
|
||||
t.Fatalf("invalid doctor flag should not write stdout, got %q", stdout)
|
||||
}
|
||||
if stderr != "[zero] unknown doctor flag \"--bogus\"\n" {
|
||||
t.Fatalf("invalid doctor flag stderr = %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// `doctor --connectivity` (a valid doctor invocation with a trailing flag) must
|
||||
// take the same doctor-shaped path: parseDoctorArgs accepts --connectivity and
|
||||
// the missing-binary fallback still reports the runtime failure.
|
||||
func TestNodeWrapperDoctorWithFlagsStillReportsDoctorFail(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, "doctor", "--connectivity")
|
||||
if exitCode != wrapperExitDoctor {
|
||||
t.Fatalf("doctor --connectivity exit = %d, want %d; stdout=%s stderr=%s", exitCode, wrapperExitDoctor, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("doctor --connectivity must write to stdout only, got stderr=%q", stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "[fail] runtime.go") {
|
||||
t.Fatalf("doctor --connectivity must still emit the failing runtime.go line, got stdout=%q", stdout)
|
||||
}
|
||||
if strings.Contains(stdout, "[zero] No native binary is available for this install") {
|
||||
t.Fatalf("doctor --connectivity must not fall back to the generic bail, got stdout=%q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperGenericReportsBunRecoveryWhenInstalledByBun(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
_, stderr, exitCode := runWrapperFixtureWithEnv(t, wrapperPath, []string{"ZERO_WRAPPER_SIMULATE_BUN=1"}, "--version")
|
||||
if exitCode != wrapperExitDoctor {
|
||||
t.Fatalf("generic missing-binary exit = %d, want %d; stderr=%s", exitCode, wrapperExitDoctor, stderr)
|
||||
}
|
||||
assertBunRecoveryMessage(t, stderr)
|
||||
if !strings.Contains(stderr, buildFromSourceLead) {
|
||||
t.Fatalf("generic bun stderr should include build-from-source guidance, got %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperDoctorReportsBunRecoveryWhenInstalledByBun(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
stdout, stderr, exitCode := runWrapperFixtureWithEnv(t, wrapperPath, []string{"ZERO_WRAPPER_SIMULATE_BUN=1"}, "doctor")
|
||||
if exitCode != wrapperExitDoctor {
|
||||
t.Fatalf("doctor bun exit = %d, want %d; stdout=%s stderr=%s", exitCode, wrapperExitDoctor, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("doctor bun report must go to stdout only, got stderr=%q", stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "[fail] runtime.go") {
|
||||
t.Fatalf("doctor bun report must still fail runtime.go, got stdout=%q", stdout)
|
||||
}
|
||||
assertBunRecoveryMessage(t, stdout)
|
||||
if !strings.Contains(stdout, buildFromSourceLead) {
|
||||
t.Fatalf("doctor bun report should reuse generic build-from-source guidance, got stdout=%q", stdout)
|
||||
}
|
||||
if strings.Contains(stdout, "If reinstall fails") {
|
||||
t.Fatalf("doctor bun report must not use a separate build-from-source string, got stdout=%q", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperDoctorAndGenericShareBunRecoveryCopy(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
simulateBun := []string{"ZERO_WRAPPER_SIMULATE_BUN=1"}
|
||||
_, genericStderr, _ := runWrapperFixtureWithEnv(t, wrapperPath, simulateBun, "--version")
|
||||
doctorStdout, _, _ := runWrapperFixtureWithEnv(t, wrapperPath, simulateBun, "doctor")
|
||||
genericBun := extractBunRecoveryBlock(genericStderr)
|
||||
doctorBun := extractBunRecoveryBlock(doctorStdout)
|
||||
if genericBun != doctorBun {
|
||||
t.Fatalf("bun recovery copy drift:\ngeneric=%q\ndoctor=%q", genericBun, doctorBun)
|
||||
}
|
||||
}
|
||||
|
||||
func assertBunRecoveryMessage(t *testing.T, output string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(output, bunRecoveryLead) {
|
||||
t.Fatalf("output should include bun recovery lead, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, bunPmTrustProject) {
|
||||
t.Fatalf("output should include project bun pm trust guidance, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, bunPmTrustGlobal) {
|
||||
t.Fatalf("output should include global bun pm trust guidance, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, bunRecoveryTrustedDeps) {
|
||||
t.Fatalf("output should include trustedDependencies fallback guidance, got %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "to your project package.json and reinstall.") {
|
||||
t.Fatalf("output should mention package.json reinstall fallback, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func extractBunRecoveryBlock(output string) string {
|
||||
start := strings.Index(output, bunRecoveryLead)
|
||||
if start < 0 {
|
||||
return ""
|
||||
}
|
||||
end := strings.Index(output[start:], buildFromSourceLead)
|
||||
if end < 0 {
|
||||
return strings.TrimSpace(output[start:])
|
||||
}
|
||||
return strings.TrimSpace(output[start : start+end])
|
||||
}
|
||||
|
||||
func TestNodeWrapperLaunchesNativeBinary(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock executable fixture uses a POSIX shell script")
|
||||
|
||||
Reference in new issue
Block a user