gentle-pi 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -12
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/orchestrator-delegation.md +19 -9
- package/assets/orchestrator.md +5 -5
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +898 -79
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +88 -12
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-integration-v2.ts +58 -8
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +87 -11
- package/runtime/review-integration-v2.mjs +58 -8
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +6 -2
- package/skills/_shared/review-ledger-contract.md +3 -1
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +28 -12
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +17 -6
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +3 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +25 -0
- package/tests/review-host-relay-routing.test.ts +20 -4
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
|
@@ -184,6 +184,44 @@ test("failure enforces exact keys, enums, and identifiers", () => {
|
|
|
184
184
|
assert.throws(() => decodeReviewFailureV2(badCode), /code/);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
test("failure with code managed_assets_outdated decodes its continuation, degrades without one, and forbids it elsewhere", () => {
|
|
188
|
+
const source: JsonObject = {
|
|
189
|
+
schema: "gentle-ai.review-integration.failure/v2",
|
|
190
|
+
contract: REVIEW_INTEGRATION_CONTRACT,
|
|
191
|
+
operation: "review.start",
|
|
192
|
+
phase: "preflight",
|
|
193
|
+
code: "managed_assets_outdated",
|
|
194
|
+
message: "the managed asset set is stale",
|
|
195
|
+
mutation_outcome: "not_started",
|
|
196
|
+
authority_applicability: "current_target",
|
|
197
|
+
retry_safe: true,
|
|
198
|
+
replayability: "manual_action_required",
|
|
199
|
+
required_inputs: [],
|
|
200
|
+
next_action: "explicit-maintainer-action",
|
|
201
|
+
continuation: { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"] },
|
|
202
|
+
};
|
|
203
|
+
assert.deepEqual(decodeReviewFailureV2(source).continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
|
|
204
|
+
|
|
205
|
+
const missingCommand = clone(source);
|
|
206
|
+
delete (missingCommand.continuation as JsonObject).command;
|
|
207
|
+
assert.throws(() => decodeReviewFailureV2(missingCommand), /command.*required|required.*command/);
|
|
208
|
+
|
|
209
|
+
// an older gentle-ai may emit managed_assets_outdated with no continuation
|
|
210
|
+
// at all; decode must not refuse the whole envelope for that
|
|
211
|
+
const missingContinuation = clone(source);
|
|
212
|
+
delete missingContinuation.continuation;
|
|
213
|
+
assert.equal(decodeReviewFailureV2(missingContinuation).continuation, undefined);
|
|
214
|
+
|
|
215
|
+
const unexpectedContinuation: JsonObject = { ...clone(source), code: "gate_scope_changed" };
|
|
216
|
+
assert.throws(() => decodeReviewFailureV2(unexpectedContinuation), /continuation is only valid/);
|
|
217
|
+
|
|
218
|
+
// an envelope without continuation, on a code that does not require one, is unchanged
|
|
219
|
+
const withoutContinuation = clone(source);
|
|
220
|
+
withoutContinuation.code = "gate_scope_changed";
|
|
221
|
+
delete withoutContinuation.continuation;
|
|
222
|
+
assert.equal(decodeReviewFailureV2(withoutContinuation).continuation, undefined);
|
|
223
|
+
});
|
|
224
|
+
|
|
187
225
|
test("net-new decoders: consent, next-transition, and artifact-subject reject malformed payloads", () => {
|
|
188
226
|
const consentSource = fixture<JsonObject>("consent.fixture.json");
|
|
189
227
|
assertRequired(decodeReviewConsentV2, consentSource, ["schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path"]);
|
|
@@ -405,6 +443,41 @@ test("next_transition decodes an execute variant and rejects a stop that carries
|
|
|
405
443
|
assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
|
|
406
444
|
});
|
|
407
445
|
|
|
446
|
+
// gentle-pi#627: gentle-ai reports a stale managed-asset set as a typed stop
|
|
447
|
+
// carrying the exact `gentle-ai sync` invocation that resolves it.
|
|
448
|
+
function managedAssetsContinuation(overrides: Partial<JsonObject> = {}): JsonObject {
|
|
449
|
+
return { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", stale_assets: ["orchestration/claude-code.md"], ...overrides };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
test("next_transition stop decodes a managed_assets_outdated continuation and rejects a malformed one", () => {
|
|
453
|
+
const stop: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: managedAssetsContinuation() };
|
|
454
|
+
const decoded = decodeReviewNextTransitionV3(stop);
|
|
455
|
+
assert.deepEqual(decoded.continuation, { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] });
|
|
456
|
+
|
|
457
|
+
const bare: JsonObject = { kind: "stop", reason_code: "managed_assets_outdated", continuation: { operation: "sync", command: "gentle-ai sync" } };
|
|
458
|
+
assert.deepEqual(decodeReviewNextTransitionV3(bare).continuation, { operation: "sync", command: "gentle-ai sync" });
|
|
459
|
+
|
|
460
|
+
const missingCommand = clone(stop);
|
|
461
|
+
delete (missingCommand.continuation as JsonObject).command;
|
|
462
|
+
assert.throws(() => decodeReviewNextTransitionV3(missingCommand), /command.*required|required.*command/);
|
|
463
|
+
|
|
464
|
+
const emptyCommand = clone(stop);
|
|
465
|
+
(emptyCommand.continuation as JsonObject).command = "";
|
|
466
|
+
assert.throws(() => decodeReviewNextTransitionV3(emptyCommand), /command/);
|
|
467
|
+
|
|
468
|
+
// an older gentle-ai may emit this stop with no continuation at all; decode
|
|
469
|
+
// must not refuse the whole envelope for that
|
|
470
|
+
const missingForReasonCode = { kind: "stop", reason_code: "managed_assets_outdated" };
|
|
471
|
+
assert.equal(decodeReviewNextTransitionV3(missingForReasonCode).continuation, undefined);
|
|
472
|
+
|
|
473
|
+
const unexpectedOnOtherReasonCode = { kind: "stop", reason_code: "rdd_disabled", continuation: managedAssetsContinuation() };
|
|
474
|
+
assert.throws(() => decodeReviewNextTransitionV3(unexpectedOnOtherReasonCode), /continuation is only valid/);
|
|
475
|
+
|
|
476
|
+
// envelopes without continuation stay unchanged
|
|
477
|
+
const plainStop = decodeReviewNextTransitionV3({ kind: "stop", reason_code: "rdd_disabled" });
|
|
478
|
+
assert.equal(plainStop.continuation, undefined);
|
|
479
|
+
});
|
|
480
|
+
|
|
408
481
|
function approvedAcknowledgementTransition(): JsonObject {
|
|
409
482
|
return {
|
|
410
483
|
kind: "execute",
|
|
@@ -700,6 +773,40 @@ test("v5 targeted-validator collect inputs carry the exact provider-owned valida
|
|
|
700
773
|
assert.throws(() => decodeReviewNextTransitionV3(weakenedRequest, { v5: true }), /policy_content/);
|
|
701
774
|
});
|
|
702
775
|
|
|
776
|
+
test("status.frozen carries the optional frozen manifest digest", () => {
|
|
777
|
+
const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
|
|
778
|
+
delete current.receipt;
|
|
779
|
+
if (current.action === "finalize") {
|
|
780
|
+
current.action = "stop";
|
|
781
|
+
delete current.next_transition;
|
|
782
|
+
delete current.forecast;
|
|
783
|
+
}
|
|
784
|
+
assert.equal(typeof (current.frozen as JsonObject | undefined)?.tier, "string", "the captured status carries frozen");
|
|
785
|
+
const digest = `sha256:${"ab".repeat(32)}`;
|
|
786
|
+
(current.frozen as JsonObject).changed_path_manifest_sha256 = digest;
|
|
787
|
+
assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, digest);
|
|
788
|
+
(current.frozen as JsonObject).changed_path_manifest_sha256 = "not-a-digest";
|
|
789
|
+
assert.throws(() => decodeReviewStatusV3(current), /changed_path_manifest_sha256/);
|
|
790
|
+
delete (current.frozen as JsonObject).changed_path_manifest_sha256;
|
|
791
|
+
assert.equal(decodeReviewStatusV3(current).frozen?.changedPathManifestSha256, undefined);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("a capture-result collect input decodes without changed_path_manifest, which the artifact subject digest already binds", () => {
|
|
795
|
+
const status = fixture<JsonObject>("status.fixture.json");
|
|
796
|
+
const transition = clone((status.next_transition ?? {}) as JsonObject);
|
|
797
|
+
const inputs = ((transition.collect as JsonObject | undefined)?.inputs ?? []) as JsonObject[];
|
|
798
|
+
const captureInput = inputs.find((input) => input.capture_operation === "review.capture-result");
|
|
799
|
+
assert.ok(captureInput, "the status fixture carries a capture-result collect input");
|
|
800
|
+
assert.doesNotThrow(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), "with the manifest");
|
|
801
|
+
delete captureInput.changed_path_manifest;
|
|
802
|
+
const decoded = decodeReviewNextTransitionV3(transition, { v5: true, v6: true });
|
|
803
|
+
const decodedInput = decoded.collect?.inputs.find((input) => input.captureOperation === "review.capture-result");
|
|
804
|
+
assert.ok(decodedInput?.artifactSubject?.changedPathManifestSha256, "the digest on the artifact subject is the binding");
|
|
805
|
+
assert.equal(decodedInput?.changedPathManifest, undefined);
|
|
806
|
+
delete captureInput.artifact_subject;
|
|
807
|
+
assert.throws(() => decodeReviewNextTransitionV3(transition, { v5: true, v6: true }), /requires artifact_subject, base_tree, and candidate_tree/);
|
|
808
|
+
});
|
|
809
|
+
|
|
703
810
|
test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
|
|
704
811
|
const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
|
|
705
812
|
delete current.receipt;
|
|
@@ -711,6 +818,11 @@ test("status enforces authority/frozen/receipt conditionals and decodes the requ
|
|
|
711
818
|
const decoded = decodeReviewStatusV3(current);
|
|
712
819
|
assert.equal(decoded.repair.status, "unsupported");
|
|
713
820
|
assert.equal(decoded.authority?.state, "reviewing");
|
|
821
|
+
for (const action of ["collect", "execute"]) {
|
|
822
|
+
const projected = clone(current);
|
|
823
|
+
projected.action = action;
|
|
824
|
+
assert.equal(decodeReviewStatusV3(projected).action, action, "a live transaction's root action names its mandated transition kind");
|
|
825
|
+
}
|
|
714
826
|
|
|
715
827
|
const missingAuthority = clone(current);
|
|
716
828
|
delete missingAuthority.authority;
|
|
@@ -331,7 +331,7 @@ test("terminal capture closes directly, nonterminal capture does not auto-follow
|
|
|
331
331
|
assert.equal(statusCalls, 2, "only initial STATUS plus one ambiguity reconciliation");
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
-
test("unknown-capture reconciliation
|
|
334
|
+
test("unknown-capture reconciliation preserves agentless legacy callers and forwards an explicit Pi agent", async () => {
|
|
335
335
|
const calls: Array<Record<string, unknown>> = [];
|
|
336
336
|
const native = {
|
|
337
337
|
targetStatus: async (request: Record<string, unknown>) => {
|
|
@@ -342,7 +342,12 @@ test("unknown-capture reconciliation validates the target-scoped lineage without
|
|
|
342
342
|
} as unknown as NativeReviewCli;
|
|
343
343
|
const result = await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA });
|
|
344
344
|
await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { baseRef: "refs/heads/main", committedOnly: true });
|
|
345
|
-
|
|
345
|
+
await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { agent: "pi" });
|
|
346
|
+
assert.deepEqual(calls, [
|
|
347
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage" },
|
|
348
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage", baseRef: "refs/heads/main", committedOnly: true },
|
|
349
|
+
{ cwd: "/repo", lineageId: "reconcile-lineage", agent: "pi" },
|
|
350
|
+
]);
|
|
346
351
|
assert.equal(result.targetIdentity, SHA);
|
|
347
352
|
let launches = 0;
|
|
348
353
|
const strictNative = new nativeReviewCliModule.NativeReviewCliV216(async () => { launches += 1; throw new Error("must not launch"); }, "/package/.gentle-ai/gentle-ai");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
|
+
import { declareReviewRelayHandshake, GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../lib/review-relay-contract.ts";
|
|
6
|
+
|
|
7
|
+
// gentle-pi#550: the session shell must inherit the relay handshake, so a
|
|
8
|
+
// `gentle-ai review ... --agent pi` typed in a shell tool is eligible without
|
|
9
|
+
// the operator reading package source for the value.
|
|
10
|
+
|
|
11
|
+
test("declareReviewRelayHandshake exports the exact contract and reports whether it changed anything", () => {
|
|
12
|
+
const env: NodeJS.ProcessEnv = {};
|
|
13
|
+
assert.equal(declareReviewRelayHandshake(env), true);
|
|
14
|
+
assert.equal(env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
15
|
+
assert.equal(declareReviewRelayHandshake(env), false, "already declared");
|
|
16
|
+
const stale: NodeJS.ProcessEnv = { [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: "gentle-pi.review-relay/v0" };
|
|
17
|
+
assert.equal(declareReviewRelayHandshake(stale), true, "a wrong value is replaced: this extension is the host");
|
|
18
|
+
assert.equal(stale[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("loading the extension declares the handshake in the session environment", () => {
|
|
22
|
+
const processEnv: NodeJS.ProcessEnv = { PATH: "/bin" };
|
|
23
|
+
createGentleAiExtension({ nativeReviewCli: null, processEnv })({ on() {}, registerTool() {}, registerCommand() {} } as unknown as ExtensionAPI);
|
|
24
|
+
assert.equal(processEnv[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV], GENTLE_PI_REVIEW_RELAY_CONTRACT);
|
|
25
|
+
assert.equal(processEnv.PATH, "/bin");
|
|
26
|
+
});
|
|
@@ -4,7 +4,7 @@ import { cpSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
|
-
import { resolveRepositoryAuthorityV1, setReviewRepositoryIdentityRetryHookForTesting } from "../lib/review-repository.ts";
|
|
7
|
+
import { resolveRepositoryAuthorityV1, reviewGitEnvironment, setReviewRepositoryIdentityRetryHookForTesting } from "../lib/review-repository.ts";
|
|
8
8
|
import { REVIEW_MODE, ReviewTransactionStore, createReviewState, setReviewMutationLockPlatformForTesting } from "../lib/review-transaction.ts";
|
|
9
9
|
import { REVIEW_LENS, REVIEW_ROUTE } from "../lib/review-triggers.ts";
|
|
10
10
|
import { qualifiedReviewLockPlatform, testSnapshot } from "./review-test-fixtures.ts";
|
|
@@ -49,6 +49,33 @@ function addOrphanRoot(root: string, branch: string, file: string): void {
|
|
|
49
49
|
git(root, "checkout", "main");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
test("review Git environment uses a Git-compatible null config path on Windows", () => {
|
|
53
|
+
const environment = reviewGitEnvironment();
|
|
54
|
+
assert.equal(environment.GIT_CONFIG_GLOBAL, "/dev/null");
|
|
55
|
+
assert.equal(environment.GIT_CONFIG_SYSTEM, "/dev/null");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("review Git environment runs a real Git authority probe", (t) => {
|
|
59
|
+
const root = repository(t);
|
|
60
|
+
const commonDirectory = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
61
|
+
cwd: root,
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
env: reviewGitEnvironment(),
|
|
64
|
+
}).trim();
|
|
65
|
+
assert.equal(resolve(commonDirectory), resolve(git(root, "rev-parse", "--path-format=absolute", "--git-common-dir")));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("review Git environment rejects inherited unsafe Git overrides", () => {
|
|
69
|
+
const prior = process.env.GIT_DIR;
|
|
70
|
+
try {
|
|
71
|
+
process.env.GIT_DIR = "/unsafe";
|
|
72
|
+
assert.throws(() => reviewGitEnvironment(), /REVIEW_GIT_ENV_UNSAFE/);
|
|
73
|
+
} finally {
|
|
74
|
+
if (prior === undefined) delete process.env.GIT_DIR;
|
|
75
|
+
else process.env.GIT_DIR = prior;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
52
79
|
test("a Windows drive-letter Git common directory resolves repository authority", { skip: process.platform !== "win32" }, (t) => {
|
|
53
80
|
const root = repository(t);
|
|
54
81
|
const commonDirectory = git(root, "rev-parse", "--path-format=absolute", "--git-common-dir");
|