gentle-pi 2.6.4 → 3.0.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 +25 -7
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +41 -19
- package/docs/readme-reference.md +175 -36
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +289 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +123 -97
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +74 -113
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +19 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +88 -0
- package/lib/session-changes.ts +147 -0
- package/lib/shell-bar.ts +24 -10
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-changes.ts +5 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +18 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +2 -3
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +321 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +883 -356
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +338 -205
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-capability-contract.test.ts +15 -1
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +119 -31
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +86 -0
- package/tests/session-changes-shell.test.ts +38 -0
- package/tests/session-changes.test.ts +114 -0
- package/tests/shell-bar.test.ts +35 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { ScrollView, visibleWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
|
-
import { renderLayoutFrame } from "@earendil-works/pi-tui/dist/layout.js";
|
|
3
|
+
import { CURSOR_MARKER, ScrollView, VStack, visibleWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
|
+
import { getScrollViewsAt, renderLayoutFrame, type LayoutBox } from "@earendil-works/pi-tui/dist/layout.js";
|
|
5
5
|
import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
6
6
|
import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
|
|
7
7
|
import { renderShellSidebarBar } from "../lib/shell-bar.ts";
|
|
@@ -32,7 +32,7 @@ test("grouped Status preserves structured fields and opaque integration text", (
|
|
|
32
32
|
}, theme, 46);
|
|
33
33
|
const text = lines.join("\n");
|
|
34
34
|
let previous = -1;
|
|
35
|
-
for (const heading of ["Status", "Project", "
|
|
35
|
+
for (const heading of ["Status", "Project", "Changes", "Usage", "Integrations"]) {
|
|
36
36
|
const index = text.indexOf(heading);
|
|
37
37
|
assert.ok(index > previous, heading);
|
|
38
38
|
previous = index;
|
|
@@ -67,13 +67,13 @@ test("only fullscreen at 140 columns activates; shrinking restores bottom paint"
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
test("rail orders Status,
|
|
70
|
+
test("rail orders unified Status, agents, TODO without standalone changes", (t) => {
|
|
71
71
|
const f = fixture();
|
|
72
72
|
for (const key of ["todo", "agents", "changes"]) {
|
|
73
73
|
sidebarPart(f.tui, key, { render: () => [key, ""], invalidate() {} });
|
|
74
74
|
}
|
|
75
75
|
t.after(installSidebar(f.tui, theme));
|
|
76
|
-
assert.deepEqual(rail(f).render(50).map((line) => line.trim()), ["✿ Gentle
|
|
76
|
+
assert.deepEqual(rail(f).render(50).map((line) => line.trim()), ["✿ Gentle Shell ✿", "", "Status", "", "agents", "", "todo"]);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
test("branding belongs to scroll content before Status, never transcript or narrow bottom", (t) => {
|
|
@@ -81,14 +81,14 @@ test("branding belongs to scroll content before Status, never transcript or narr
|
|
|
81
81
|
t.after(installSidebar(f.tui, theme));
|
|
82
82
|
const scroll = rail(f);
|
|
83
83
|
const lines = scroll.render(50);
|
|
84
|
-
const brandIndex = lines.findIndex((line) => line.includes("✿ Gentle
|
|
84
|
+
const brandIndex = lines.findIndex((line) => line.includes("✿ Gentle Shell ✿"));
|
|
85
85
|
assert.ok(brandIndex >= 0 && brandIndex < lines.findIndex((line) => line.includes("Status")));
|
|
86
86
|
assert.doesNotMatch(lines.join("\n"), /[\u2800-\u28ff]/);
|
|
87
87
|
const heading = lines[brandIndex];
|
|
88
88
|
const usableWidth = scroll.getContentWidth(50) - 2;
|
|
89
|
-
const spare = usableWidth - visibleWidth("✿ Gentle
|
|
89
|
+
const spare = usableWidth - visibleWidth("✿ Gentle Shell ✿");
|
|
90
90
|
const scrollbarWidth = 50 - scroll.getContentWidth(50);
|
|
91
|
-
assert.equal(heading, " ".repeat(1 + Math.floor(spare / 2)) + "✿ Gentle
|
|
91
|
+
assert.equal(heading, " ".repeat(1 + Math.floor(spare / 2)) + "✿ Gentle Shell ✿" + " ".repeat(1 + Math.ceil(spare / 2) + scrollbarWidth));
|
|
92
92
|
assert.deepEqual(f.root.render(), ["transcript"]);
|
|
93
93
|
scroll.updateLayout(lines.length, 2, () => {});
|
|
94
94
|
scroll.scrollBy(9);
|
|
@@ -249,30 +249,30 @@ test("real layout frames reuse unchanged sidebar output and invalidate at state
|
|
|
249
249
|
t.after(installSidebar(f.tui, theme));
|
|
250
250
|
|
|
251
251
|
const first = renderLayoutFrame(f.root, 140, 20, () => {});
|
|
252
|
-
assert.deepEqual(counts, { footer: 1, changes:
|
|
252
|
+
assert.deepEqual(counts, { footer: 1, changes: 0, agents: 1, todo: 1 });
|
|
253
253
|
const sidebarRail = first.root.children[1]?.component as ScrollView;
|
|
254
254
|
renderLayoutFrame(f.root, 140, 20, () => {});
|
|
255
|
-
assert.deepEqual(counts, { footer: 1, changes:
|
|
255
|
+
assert.deepEqual(counts, { footer: 1, changes: 0, agents: 1, todo: 1 });
|
|
256
256
|
|
|
257
257
|
todo = "Todo two";
|
|
258
258
|
sidebarRail.invalidate();
|
|
259
259
|
const changed = renderLayoutFrame(f.root, 140, 20, () => {});
|
|
260
260
|
assert.match(changed.lines.join("\n"), /Todo two/);
|
|
261
|
-
assert.deepEqual(counts, { footer: 2, changes:
|
|
261
|
+
assert.deepEqual(counts, { footer: 2, changes: 0, agents: 2, todo: 2 });
|
|
262
262
|
|
|
263
263
|
f.host.terminal.columns = 139;
|
|
264
264
|
renderLayoutFrame(f.root, 139, 20, () => {});
|
|
265
265
|
assert.deepEqual(f.bottom.render(80), ["Status"]);
|
|
266
266
|
f.host.terminal.columns = 140;
|
|
267
267
|
renderLayoutFrame(f.root, 140, 20, () => {});
|
|
268
|
-
assert.deepEqual(counts, { footer: 3, changes:
|
|
268
|
+
assert.deepEqual(counts, { footer: 3, changes: 0, agents: 3, todo: 3 });
|
|
269
269
|
|
|
270
270
|
f.host.mode = "regular";
|
|
271
271
|
renderLayoutFrame(f.root, 140, 20, () => {});
|
|
272
272
|
assert.deepEqual(f.bottom.render(80), ["Status"]);
|
|
273
273
|
f.host.mode = "fullscreen";
|
|
274
274
|
renderLayoutFrame(f.root, 140, 20, () => {});
|
|
275
|
-
assert.deepEqual(counts, { footer: 4, changes:
|
|
275
|
+
assert.deepEqual(counts, { footer: 4, changes: 0, agents: 4, todo: 4 });
|
|
276
276
|
|
|
277
277
|
let replacementRenders = 0;
|
|
278
278
|
sidebarPart(f.tui, "todo", {
|
|
@@ -385,3 +385,87 @@ test("unsupported roots, empty rails and overflowing parts leave native layout i
|
|
|
385
385
|
t.after(installSidebar(f.tui, theme));
|
|
386
386
|
assert.deepEqual(f.bottom.render(80), ["Status"]);
|
|
387
387
|
});
|
|
388
|
+
|
|
389
|
+
// A real native subtree exposes duplicate rendering hidden by empty-layout fixtures.
|
|
390
|
+
function nativeSidebarFixture(legacyMeasurement = false) {
|
|
391
|
+
let transcriptRenders = 0;
|
|
392
|
+
let editorRows = 1;
|
|
393
|
+
const transcript = {
|
|
394
|
+
render(width: number) {
|
|
395
|
+
transcriptRenders++;
|
|
396
|
+
return Array.from({ length: 1000 }, (_, index) => `\x1b[32mEntry ${index} 界 é at ${width}\x1b[0m`);
|
|
397
|
+
},
|
|
398
|
+
invalidate() {},
|
|
399
|
+
};
|
|
400
|
+
const primary = new ScrollView(transcript, { primary: true, follow: "end", scrollbar: "always" });
|
|
401
|
+
const editor = { render: () => Array.from({ length: editorRows }, (_, index) => `Editor ${index}${index === editorRows - 1 ? CURSOR_MARKER : ""}`), invalidate() {} };
|
|
402
|
+
const root = new VStack([{ component: primary, basis: 0, grow: 1 }, { component: editor }]);
|
|
403
|
+
const originalRender = root.render;
|
|
404
|
+
const host = { mode: "fullscreen", terminal: { columns: 180 }, layoutRoot: root, requestRender() {} };
|
|
405
|
+
const tui = host as unknown as TUI;
|
|
406
|
+
sidebarPart(tui, "footer", { render: () => Array.from({ length: 100 }, (_, i) => `Status ${i}`), invalidate() {} });
|
|
407
|
+
const dispose = installSidebar(tui, theme);
|
|
408
|
+
const nativeRoot = root as unknown as { [NODE](): { entries: Array<{ component: { render(width: number): string[] } }> } };
|
|
409
|
+
if (legacyMeasurement) nativeRoot[NODE]().entries[0].component.render = (width) => root.render(width);
|
|
410
|
+
return {
|
|
411
|
+
root, primary, host, dispose, originalRender,
|
|
412
|
+
growEditor: () => { editorRows = 4; },
|
|
413
|
+
frame(width = host.terminal.columns, height = 30) {
|
|
414
|
+
host.terminal.columns = width;
|
|
415
|
+
transcriptRenders = 0;
|
|
416
|
+
const frame = renderLayoutFrame(root, width, height, () => {});
|
|
417
|
+
return { frame, transcriptRenders };
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function layoutGeometry(box: LayoutBox): unknown {
|
|
423
|
+
return { rect: box.rect, clip: box.clip, lineOffset: box.lineOffset, scroll: !!box.scrollView, children: box.children.map(layoutGeometry) };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
test("fullscreen native transcript renders once without changing bytes, geometry or primary scroll", (t) => {
|
|
427
|
+
const current = nativeSidebarFixture();
|
|
428
|
+
const legacy = nativeSidebarFixture(true);
|
|
429
|
+
t.after(current.dispose);
|
|
430
|
+
t.after(legacy.dispose);
|
|
431
|
+
for (const grow of [false, true]) {
|
|
432
|
+
if (grow) { current.growEditor(); legacy.growEditor(); }
|
|
433
|
+
for (const [width, height] of [[180, 30], [140, 18], [220, 50]]) {
|
|
434
|
+
const actual = current.frame(width, height);
|
|
435
|
+
const expected = legacy.frame(width, height);
|
|
436
|
+
assert.equal(actual.transcriptRenders, 1, "measurement must not render the native transcript");
|
|
437
|
+
assert.equal(expected.transcriptRenders, 2, "legacy control must reproduce the redundant render");
|
|
438
|
+
assert.deepEqual(actual.frame.lines, expected.frame.lines);
|
|
439
|
+
assert.deepEqual(layoutGeometry(actual.frame.root), layoutGeometry(expected.frame.root));
|
|
440
|
+
assert.equal(actual.frame.primaryScrollView, current.primary);
|
|
441
|
+
assert.equal(current.root.render, current.originalRender, "native root rendering is not patched");
|
|
442
|
+
assert.ok(actual.frame.lines.some(line => line.includes(CURSOR_MARKER)), "editor cursor remains visible");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("native transcript and sidebar keep separate scroll routing across resize, breakpoint and mode transitions", (t) => {
|
|
448
|
+
const fixture = nativeSidebarFixture();
|
|
449
|
+
t.after(fixture.dispose);
|
|
450
|
+
let result = fixture.frame();
|
|
451
|
+
const rail = result.frame.root.children[1].component as ScrollView;
|
|
452
|
+
assert.equal(getScrollViewsAt(result.frame, 1, 1)[0], fixture.primary);
|
|
453
|
+
assert.equal(getScrollViewsAt(result.frame, 179, 1)[0], rail);
|
|
454
|
+
const transcriptTop = fixture.primary.scrollTop;
|
|
455
|
+
rail.scrollBy(5);
|
|
456
|
+
result = fixture.frame();
|
|
457
|
+
assert.equal(fixture.primary.scrollTop, transcriptTop);
|
|
458
|
+
assert.equal(rail.scrollTop, 5);
|
|
459
|
+
fixture.primary.scrollBy(-7);
|
|
460
|
+
result = fixture.frame();
|
|
461
|
+
assert.equal(fixture.primary.scrollTop, transcriptTop - 7);
|
|
462
|
+
assert.equal(rail.scrollTop, 5);
|
|
463
|
+
assert.equal(result.transcriptRenders, 1);
|
|
464
|
+
for (const [mode, width] of [["fullscreen", 139], ["fullscreen", 140], ["regular", 180], ["fullscreen", 180]] as const) {
|
|
465
|
+
fixture.host.mode = mode;
|
|
466
|
+
result = fixture.frame(width);
|
|
467
|
+
assert.equal(result.transcriptRenders, 1);
|
|
468
|
+
assert.equal(result.frame.primaryScrollView, fixture.primary);
|
|
469
|
+
assert.equal(result.frame.root.children.some(box => box.component === rail), mode === "fullscreen" && width >= 140);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
@@ -2,11 +2,63 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { syncBuiltinESMExports } from "node:module";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
5
6
|
import startup, { readGitBranch } from "../extensions/startup-banner.ts";
|
|
6
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
9
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
9
10
|
|
|
11
|
+
test("startup artwork spells Gentle Shell with aligned animation spans", () => {
|
|
12
|
+
const source = readFileSync(new URL("../extensions/startup-banner.ts", import.meta.url), "utf8");
|
|
13
|
+
const logo = JSON.parse(source.match(/const TEXT_LOGO = (\[[\s\S]*?\]);/)![1].replace(/,\s*]/, "]")) as string[];
|
|
14
|
+
const weights = JSON.parse(source.match(/const LETTER_WEIGHTS = (\[[^;]+\]);/)![1]) as number[];
|
|
15
|
+
// Preserve the original script, including its descending G and dark shadow.
|
|
16
|
+
const gentle = [
|
|
17
|
+
" ▄▄▄▀▀▀▀▀██ ▄▄▀▄▄",
|
|
18
|
+
" ▄▄█▀▀▒▒▒▒▒▄▄█▀▒ ▄██ ▄▄█▀█▄█▀▒▒",
|
|
19
|
+
" ▄▄██▀▒▒▒▒▒▄▄▄▀▀▒▒▒▒ ▄▄▄ ▀▀▀▀██▀▀▀▀▀███▀█▄▀▀▒▒▒▒",
|
|
20
|
+
" ▄██▀▒▒▒▒ ▒▒▄▄█ ▄▄▄▀██ ▄▄▄▀▀▀▄ ▄██▀▒▒▒▒▄██▀▀▀▒▄▄███",
|
|
21
|
+
" ██▀▒▒▒ ▄▄▄███▀▄██▀▀▀▄▄██▀▀▄█▀▄▄██▀▒▒▒▄▄██▀▒▒▄██▀▀▀▄▄",
|
|
22
|
+
" ▀█▄▄▄▄▄▀▀▀█▄▄███▄▒▀▀▀▀▀▀▒▀▀▒▒▀▀▀▀▒██▄▄▀▀▀ ▀█▄▀▀▀ ▀▀▀▀▀▒▒",
|
|
23
|
+
" ▒▄▄▄█▀▀▀█▄█▀▀▒▒▒▒ ▒▒▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒",
|
|
24
|
+
" ▄▄▀▀ ▒▒▒▒▄██▀▒▒▒▒",
|
|
25
|
+
" ▄█ ▒▒▒▄▄██▀▀▒▒▒▒",
|
|
26
|
+
" ▀▀▀▀▀▀▒▒▒▒▒▒",
|
|
27
|
+
" ▒▒▒▒▒▒",
|
|
28
|
+
];
|
|
29
|
+
assert.equal(logo.length, 11, "retain the full-height script silhouette");
|
|
30
|
+
for (const [row, prefix] of gentle.entries()) {
|
|
31
|
+
assert.ok(logo[row].startsWith(prefix), `original Gentle script row ${row}`);
|
|
32
|
+
assert.ok(logo[row].includes("▒") || row === 0, "retain dark shadow");
|
|
33
|
+
}
|
|
34
|
+
assert.equal(weights.length, 11, "one variable-width span per GENTLESHELL letter");
|
|
35
|
+
assert.ok(new Set(weights).size > 2, "script spans must not use fixed block-font widths");
|
|
36
|
+
assert.ok(logo.slice(0, 7).every((line) => /[▄▀█]/.test(line.slice(68))), "Shell has tall slanted strokes");
|
|
37
|
+
assert.ok(logo.slice(1, 7).every((line) => /▒/.test(line.slice(68))), "Shell retains the dark shadow");
|
|
38
|
+
const shell = logo.map((line) => line.slice(68));
|
|
39
|
+
assert.match(shell[0], /▄▄█▀▀▀██/, "S has its upper bowl");
|
|
40
|
+
assert.match(shell[3], /▀▀▀██▄▄/, "S curves into its lower bowl");
|
|
41
|
+
assert.match(shell[5], /▀█▄▄▄▄█▀▀▒▒/, "S closes with a shadowed exit stroke");
|
|
42
|
+
assert.match(shell[4], /▄██▀▄██▀██▒/, "h has a rising stem and connected arch");
|
|
43
|
+
assert.deepEqual(logo.slice(3, 6).map((line) => line.slice(93, 101)), [
|
|
44
|
+
" ▄▄▀▀██ ",
|
|
45
|
+
"▄██▄▄▀▒ ",
|
|
46
|
+
"▀█▄▄▄▄▀▀",
|
|
47
|
+
], "e has an upper loop/counter, a crossbar, and a curved lower exit rather than a wedge");
|
|
48
|
+
assert.equal(logo[3][100], " ", "e loop stays distinct from the following l");
|
|
49
|
+
assert.equal(logo[4][100], " ", "e counter opens before the l stem without moving it");
|
|
50
|
+
assert.equal(shell[0].split("▄▄▀▄▄").length - 1, 2, "both l ascenders retain looped tops");
|
|
51
|
+
assert.match(shell[5], /█▄▄▀▀▀▄▄▀█▄▄▀▀▀$/, "both l exits connect with an ascending script stroke");
|
|
52
|
+
assert.equal(Math.max(...logo.map((line) => line.length)), 120, "connected script retains glyph scale while tightening letter placement");
|
|
53
|
+
// Like Gentle, adjacent baseline strokes must meet, not merely have smaller gaps.
|
|
54
|
+
assert.match(shell[4], /▀██▄▄██▀▄██▀██▒/, "S exit meets the h entry");
|
|
55
|
+
assert.match(shell[5], /▀█▄▄▄▄▀▀█▄▄▀▀▀/, "e lower exit still meets the unchanged l stem");
|
|
56
|
+
assert.match(shell[5], /██▄▄▀▀█▄▄▄▄▀▀/, "h exit remains connected to e");
|
|
57
|
+
assert.deepEqual(weights, [22, 9, 8, 9, 8, 9, 10, 13, 8, 8, 13]);
|
|
58
|
+
assert.equal(weights.reduce((sum, width) => sum + width, 0), Math.max(...logo.map((line) => line.length)) - 3,
|
|
59
|
+
"span widths exactly cover the ink bounds from the G descender to the final l shadow");
|
|
60
|
+
});
|
|
61
|
+
|
|
10
62
|
test("startup branch lookup uses direct git argv and hides its Windows child", async () => {
|
|
11
63
|
const calls: Array<{ command: string; args: readonly string[]; options: Record<string, unknown> }> = [];
|
|
12
64
|
const run = ((command: string, args: readonly string[], options: Record<string, unknown>, callback: (error: Error | null, stdout: string) => void) => {
|
|
@@ -46,6 +98,7 @@ test("startup banner keeps animating after invalidate and cleans up on dispose",
|
|
|
46
98
|
header = factory({ requestRender() { renders++; } }, { fg: (_role: string, text: string) => text });
|
|
47
99
|
} } });
|
|
48
100
|
t.mock.timers.tick(50);
|
|
101
|
+
assert.match(header!.render(200).join("\n"), /\x1b\[38;2;95;30;60m▒/, "script shadow keeps the dark pink palette");
|
|
49
102
|
const afterBoot = renders;
|
|
50
103
|
t.mock.timers.tick(25);
|
|
51
104
|
assert.ok(renders > afterBoot, "animation timer requests renders");
|
|
@@ -91,13 +144,13 @@ for (const showRose of [false, true]) for (const showTextLogo of [false, true])
|
|
|
91
144
|
} } });
|
|
92
145
|
t.mock.timers.tick(50);
|
|
93
146
|
try {
|
|
94
|
-
for (const width of [40, 80, 160]) {
|
|
147
|
+
for (const width of [40, 80, 160, 200]) {
|
|
95
148
|
const lines = header!.render(width);
|
|
96
149
|
assert.ok(lines.every((line) => visibleWidth(line) <= width));
|
|
97
150
|
const text = stripAnsi(lines.join("\n"));
|
|
98
151
|
assert.match(text, /GIT:/);
|
|
99
152
|
assert.match(text, /PATH:/);
|
|
100
|
-
if (width
|
|
153
|
+
if (width >= 160) {
|
|
101
154
|
assert.equal(/[\u2800-\u28ff]/.test(text), showRose);
|
|
102
155
|
assert.equal(/[▒▄▀█]/.test(text), showTextLogo);
|
|
103
156
|
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import type { TestContext } from "node:test";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import type { ReceiptBodyV1 } from "../lib/review-transaction.ts";
|
|
10
|
+
|
|
11
|
+
type Options = Record<string, unknown>;
|
|
12
|
+
type Capture = {
|
|
13
|
+
kind: "execFileSync" | "execFile" | "spawnSync";
|
|
14
|
+
command: string;
|
|
15
|
+
args: readonly string[];
|
|
16
|
+
options: Options;
|
|
17
|
+
};
|
|
18
|
+
type Trace = { label: string; captures: Capture[] };
|
|
19
|
+
type ExpectedCall = Pick<Capture, "kind" | "command" | "args">;
|
|
20
|
+
|
|
21
|
+
async function trace<T>(captures: Capture[], traces: Trace[], label: string, action: () => T | Promise<T>, expected: readonly ExpectedCall[]): Promise<T> {
|
|
22
|
+
const start = captures.length;
|
|
23
|
+
const result = await action();
|
|
24
|
+
const route = captures.slice(start);
|
|
25
|
+
assert.deepEqual(route.map(({ kind, command, args }) => ({ kind, command, args })), expected, `${label} must use its documented public route`);
|
|
26
|
+
traces.push({ label, captures: route });
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function childProcess(): typeof import("node:child_process") {
|
|
31
|
+
return createRequire(import.meta.url)("node:child_process") as typeof import("node:child_process");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fakeGitResult(command: string, args: readonly string[], cwd: string): string {
|
|
35
|
+
if (command !== "git") return "";
|
|
36
|
+
if (args.includes("--show-toplevel")) return cwd;
|
|
37
|
+
if (args.includes("--git-common-dir")) return `${cwd}/.git`;
|
|
38
|
+
if (args.includes("--show-object-format")) return "sha1";
|
|
39
|
+
if (args.includes("--is-shallow-repository")) return "false";
|
|
40
|
+
if (args.includes("--git-path")) return join(cwd, ".git", "gentle-ai", "reviews");
|
|
41
|
+
if (args.includes("--max-parents=0")) return "a".repeat(40);
|
|
42
|
+
if (args[0] === "cat-file" && args[1] === "-t") return "tree";
|
|
43
|
+
if (args.includes("--verify")) {
|
|
44
|
+
const object = args.find((value) => value.endsWith("^{object}"));
|
|
45
|
+
return object === `${"b".repeat(40)}^{object}` ? "b".repeat(40) : "b".repeat(40);
|
|
46
|
+
}
|
|
47
|
+
if (args.includes("ls-files")) return "tests/windows-hidden-processes.test.ts\0";
|
|
48
|
+
throw new Error(`windows-hidden fixture: unexpected execFileSync git argv ${args.join(" ")}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function approvedReceipt(
|
|
52
|
+
transaction: typeof import("../lib/review-transaction.ts"),
|
|
53
|
+
fixtures: typeof import("./review-test-fixtures.ts"),
|
|
54
|
+
tree: string,
|
|
55
|
+
) {
|
|
56
|
+
const current = transaction.createReviewState({
|
|
57
|
+
lineageId: "windows-hidden-gate",
|
|
58
|
+
mode: "ordinary",
|
|
59
|
+
snapshot: fixtures.testSnapshot({
|
|
60
|
+
baseTree: "1".repeat(40),
|
|
61
|
+
completeTree: "2".repeat(40),
|
|
62
|
+
initialTree: "3".repeat(40),
|
|
63
|
+
route: "standard",
|
|
64
|
+
lenses: ["review-risk"],
|
|
65
|
+
}),
|
|
66
|
+
evidenceHash: "b".repeat(64),
|
|
67
|
+
budget: { review_batches: 1, review_actors: 1, refuter_batches: 1, fix_batches: 1, validator_runs: 1, final_verifications: 1, judgment_rounds: 0, judge_runs: 0 },
|
|
68
|
+
});
|
|
69
|
+
const body: ReceiptBodyV1 = {
|
|
70
|
+
schema: "gentle-ai.review-receipt-body/v1",
|
|
71
|
+
lineage_id: current.lineage_id,
|
|
72
|
+
mode: current.mode,
|
|
73
|
+
base_tree: current.base_tree,
|
|
74
|
+
complete_snapshot_tree: current.complete_snapshot_tree,
|
|
75
|
+
review_projection: current.review_projection,
|
|
76
|
+
initial_review_tree: current.initial_review_tree,
|
|
77
|
+
final_candidate_tree: tree,
|
|
78
|
+
route: current.route,
|
|
79
|
+
lenses: current.lenses,
|
|
80
|
+
policy_hash: current.policy_hash,
|
|
81
|
+
frozen_ledger_hash: "c".repeat(64),
|
|
82
|
+
evidence_hash: current.evidence_hash,
|
|
83
|
+
budget: current.budget,
|
|
84
|
+
counters: current.counters,
|
|
85
|
+
terminal_state: "approved",
|
|
86
|
+
};
|
|
87
|
+
return transaction.createReceiptEnvelope(body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fakeSpawnResult(command: string, args: readonly string[], cwd: string): { status: number; stdout: string } {
|
|
91
|
+
if (command !== "git") return { status: 0, stdout: "" };
|
|
92
|
+
if (args.join(" ") === "rev-parse --show-toplevel") return { status: 0, stdout: `${cwd}\n` };
|
|
93
|
+
if (args.length === 3 && args[0] === "-C" && args[2] === "remote") return { status: 0, stdout: "origin\n" };
|
|
94
|
+
if (args.includes("get-url")) return { status: 0, stdout: "https://example.invalid/repo.git\n" };
|
|
95
|
+
if (args.includes("check-ref-format")) return { status: 0, stdout: "" };
|
|
96
|
+
if (args.includes("ls-remote")) return { status: 0, stdout: `${"c".repeat(40)}\trefs/heads/main\n` };
|
|
97
|
+
if (args[2] === "config") return { status: 0, stdout: "" };
|
|
98
|
+
throw new Error(`windows-hidden fixture: unexpected spawnSync git argv ${args.join(" ")}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function captureOwnedRoutes(t: TestContext): Promise<{ captures: Capture[]; traces: Trace[] }> {
|
|
102
|
+
const cp = childProcess();
|
|
103
|
+
const original = { execFileSync: cp.execFileSync, execFile: cp.execFile, spawnSync: cp.spawnSync };
|
|
104
|
+
const captures: Capture[] = [];
|
|
105
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-windows-hidden-workspace-"));
|
|
106
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
107
|
+
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
108
|
+
const fakeSync = ((command: string, args: readonly string[], options: Options = {}) => {
|
|
109
|
+
captures.push({ kind: "execFileSync", command, args, options });
|
|
110
|
+
return fakeGitResult(command, args, String(options.cwd ?? cwd));
|
|
111
|
+
}) as unknown as typeof cp.execFileSync;
|
|
112
|
+
const fakeAsync = function(command: string, args: readonly string[], options: Options, callback: (error: null, stdout: string, stderr: string) => void) {
|
|
113
|
+
captures.push({ kind: "execFile", command, args, options });
|
|
114
|
+
if (process.platform === "win32" && command === "codegraph") {
|
|
115
|
+
callback(Object.assign(new Error("fixture shim missing"), { code: "ENOENT" }) as never, "", "");
|
|
116
|
+
} else {
|
|
117
|
+
callback(null, "indexed", "");
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
} as unknown as typeof cp.execFile;
|
|
121
|
+
(fakeAsync as unknown as { [promisify.custom]?: unknown })[promisify.custom] = async (command: string, args: readonly string[], options: Options) => {
|
|
122
|
+
captures.push({ kind: "execFile", command, args, options });
|
|
123
|
+
if (process.platform === "win32" && command === "codegraph") throw Object.assign(new Error("fixture shim missing"), { code: "ENOENT" });
|
|
124
|
+
return { stdout: "indexed", stderr: "" };
|
|
125
|
+
};
|
|
126
|
+
const fakeSpawn = ((command: string, args: readonly string[], options: Options = {}) => {
|
|
127
|
+
const result = fakeSpawnResult(command, args, String(options.cwd ?? cwd));
|
|
128
|
+
captures.push({ kind: "spawnSync", command, args, options });
|
|
129
|
+
if (command === "gh") return { status: 0, stdout: '{"total_count":0,"returned":0,"checks":[]}' };
|
|
130
|
+
return { ...result, stderr: "" };
|
|
131
|
+
}) as unknown as typeof cp.spawnSync;
|
|
132
|
+
try {
|
|
133
|
+
cp.execFileSync = fakeSync;
|
|
134
|
+
cp.execFile = fakeAsync;
|
|
135
|
+
cp.spawnSync = fakeSpawn;
|
|
136
|
+
syncBuiltinESMExports();
|
|
137
|
+
let fallbackRoot: string | undefined;
|
|
138
|
+
let fallbackCalls: ExpectedCall[] = [];
|
|
139
|
+
if (process.platform === "win32") {
|
|
140
|
+
fallbackRoot = mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-contract-"));
|
|
141
|
+
t.after(() => rmSync(fallbackRoot!, { recursive: true, force: true }));
|
|
142
|
+
const packageRoot = join(fallbackRoot, "node_modules", "@colbymchenry", "codegraph");
|
|
143
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
144
|
+
writeFileSync(join(fallbackRoot, "codegraph.cmd"), "@ECHO off\n");
|
|
145
|
+
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ bin: { codegraph: "entry.js" } }));
|
|
146
|
+
writeFileSync(join(packageRoot, "entry.js"), "// fixture entry\n");
|
|
147
|
+
const previousPath = process.env.Path;
|
|
148
|
+
const previousUpperPath = process.env.PATH;
|
|
149
|
+
t.after(() => { if (previousPath === undefined) delete process.env.Path; else process.env.Path = previousPath; });
|
|
150
|
+
t.after(() => { if (previousUpperPath === undefined) delete process.env.PATH; else process.env.PATH = previousUpperPath; });
|
|
151
|
+
process.env.Path = fallbackRoot;
|
|
152
|
+
process.env.PATH = fallbackRoot;
|
|
153
|
+
fallbackCalls = [{ kind: "execFile", command: process.execPath, args: [join(fallbackRoot, "node_modules", "@colbymchenry", "codegraph", "entry.js"), "init", cwd] }];
|
|
154
|
+
}
|
|
155
|
+
// The fixed query prevents accidental cache misses from becoming a fake proof.
|
|
156
|
+
const query = "?windows-hidden-contract";
|
|
157
|
+
const codegraph = await import(`../extensions/codegraph-tools.ts${query}`);
|
|
158
|
+
const repository = await import(`../lib/review-repository.ts${query}`);
|
|
159
|
+
const snapshot = await import(`../lib/review-snapshot.ts${query}`);
|
|
160
|
+
const transaction = await import(`../lib/review-transaction.ts${query}`);
|
|
161
|
+
const fixtures = await import(`./review-test-fixtures.ts${query}`);
|
|
162
|
+
const publication = await import(`../lib/review-publication-gate.ts${query}`);
|
|
163
|
+
const gentleAi = await import(`../extensions/gentle-ai.ts${query}`);
|
|
164
|
+
|
|
165
|
+
const traces: Trace[] = [];
|
|
166
|
+
const graph = codegraph.createCodeGraphTool();
|
|
167
|
+
assert.equal(typeof graph.execute, "function", "CodeGraph public handler must be registered");
|
|
168
|
+
const graphResult = await trace(captures, traces, "CodeGraph init", () => graph.execute("capture", { operation: "init" }, undefined, undefined, { cwd } as never), [
|
|
169
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
170
|
+
{ kind: "execFile", command: "codegraph", args: ["init", cwd] },
|
|
171
|
+
...fallbackCalls,
|
|
172
|
+
]);
|
|
173
|
+
const graphText = String((graphResult as { content?: Array<{ text?: string }> }).content?.[0]?.text ?? "");
|
|
174
|
+
assert.match(graphText, /indexed/, "CodeGraph must return the fixture command result");
|
|
175
|
+
|
|
176
|
+
const tools = new Map<string, { execute: (...args: unknown[]) => Promise<unknown> }>();
|
|
177
|
+
gentleAi.createGentleAiExtension({ nativeReviewCli: { targetStatus: async () => ({}) } as never })({
|
|
178
|
+
registerTool(definition: { name: string; execute: (...args: unknown[]) => Promise<unknown> }) { tools.set(definition.name, definition); },
|
|
179
|
+
registerCommand() {}, on() {},
|
|
180
|
+
} as never);
|
|
181
|
+
const review = tools.get("gentle_review");
|
|
182
|
+
assert.ok(review, "gentle_review public handler must be registered");
|
|
183
|
+
await trace(captures, traces, "gentle_review inspect", () => review.execute("capture", { operation: "inspect", workspaceRoot: cwd }, undefined, undefined, { cwd, hasUI: false } as never), [
|
|
184
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
185
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--git-common-dir"] },
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const authority = await trace(captures, traces, "repository authority", () => repository.reviewStoreRootForRepositoryV1(cwd), [
|
|
189
|
+
{ kind: "execFileSync", command: "git", args: ["-C", cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"] },
|
|
190
|
+
{ kind: "execFileSync", command: "git", args: ["-C", cwd, "rev-parse", "--show-object-format"] },
|
|
191
|
+
{ kind: "execFileSync", command: "git", args: ["-C", cwd, "rev-parse", "--is-shallow-repository"] },
|
|
192
|
+
{ kind: "execFileSync", command: "git", args: ["-C", cwd, "rev-list", "--max-parents=0", "--all"] },
|
|
193
|
+
]);
|
|
194
|
+
assert.equal(authority, join(cwd, ".git", "gentle-ai", "reviews"));
|
|
195
|
+
const untracked = await trace(captures, traces, "snapshot untracked discovery", () => snapshot.discoverReviewUntrackedPaths(cwd), [
|
|
196
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
197
|
+
{ kind: "execFileSync", command: "git", args: ["ls-files", "--others", "--exclude-standard", "-z"] },
|
|
198
|
+
]);
|
|
199
|
+
assert.deepEqual(untracked, ["tests/windows-hidden-processes.test.ts"]);
|
|
200
|
+
const storeRoot = await trace(captures, traces, "transaction store root", () => transaction.reviewStoreRootForRepository(cwd), [
|
|
201
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
202
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--git-path", "gentle-ai/reviews"] },
|
|
203
|
+
]);
|
|
204
|
+
assert.equal(storeRoot, join(cwd, ".git", "gentle-ai", "reviews"));
|
|
205
|
+
const gateTree = "b".repeat(40);
|
|
206
|
+
const gate = await trace(captures, traces, "transaction intended-commit gate", () => transaction.evaluateGateTarget(
|
|
207
|
+
approvedReceipt(transaction, fixtures, gateTree),
|
|
208
|
+
{ kind: publication.GATE_TARGET_KIND.INTENDED_COMMIT, intended_commit_tree: gateTree }, cwd, gateTree,
|
|
209
|
+
), [
|
|
210
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
211
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--verify", `${gateTree}^{object}`] },
|
|
212
|
+
{ kind: "execFileSync", command: "git", args: ["cat-file", "-t", gateTree] },
|
|
213
|
+
{ kind: "execFileSync", command: "git", args: ["rev-parse", "--verify", `${gateTree}^{object}`] },
|
|
214
|
+
{ kind: "execFileSync", command: "git", args: ["cat-file", "-t", gateTree] },
|
|
215
|
+
]);
|
|
216
|
+
assert.equal(gate.status, publication.GATE_RESULT.ALLOW);
|
|
217
|
+
const destination = await trace(captures, traces, "publication configured destination", () => publication.resolveConfiguredPushDestinationV1(cwd, "origin"), [
|
|
218
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote"] },
|
|
219
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote", "get-url", "--push", "--all", "origin"] },
|
|
220
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "config", "--get-all", "remote.origin.pushurl"] },
|
|
221
|
+
]);
|
|
222
|
+
assert.equal(destination.url, "https://example.invalid/repo.git");
|
|
223
|
+
await trace(captures, traces, "publication remote ref", () => publication.resolvePushRemoteRefV1(cwd, "origin", "refs/heads/main", "remote ref"), [
|
|
224
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote"] },
|
|
225
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote", "get-url", "--push", "--all", "origin"] },
|
|
226
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "config", "--get-all", "remote.origin.pushurl"] },
|
|
227
|
+
{ kind: "spawnSync", command: "git", args: ["ls-remote", "--refs", "https://example.invalid/repo.git", "refs/heads/main"] },
|
|
228
|
+
]);
|
|
229
|
+
await trace(captures, traces, "publication destination ref", () => publication.resolvePushDestinationRefV1(cwd, "origin", "main", "refs/heads/main", "destination"), [
|
|
230
|
+
{ kind: "spawnSync", command: "git", args: ["check-ref-format", "refs/main"] },
|
|
231
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote"] },
|
|
232
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote", "get-url", "--push", "--all", "origin"] },
|
|
233
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "config", "--get-all", "remote.origin.pushurl"] },
|
|
234
|
+
{ kind: "spawnSync", command: "git", args: ["ls-remote", "--refs", "https://example.invalid/repo.git"] },
|
|
235
|
+
]);
|
|
236
|
+
await trace(captures, traces, "publication advertised object", () => publication.pushRemoteAdvertisesObjectV1(cwd, "origin", destination.destination_id, "c".repeat(40)), [
|
|
237
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote"] },
|
|
238
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote", "get-url", "--push", "--all", "origin"] },
|
|
239
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "config", "--get-all", "remote.origin.pushurl"] },
|
|
240
|
+
{ kind: "spawnSync", command: "git", args: ["ls-remote", "--refs", "https://example.invalid/repo.git"] },
|
|
241
|
+
]);
|
|
242
|
+
const remoteHead = await trace(captures, traces, "publication remote head", () => publication.recheckReleaseFastPathRemoteHeadV1({ repositoryCwd: cwd, remote: "origin", expectedRemoteHead: "c".repeat(40) }), [
|
|
243
|
+
{ kind: "spawnSync", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
244
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote"] },
|
|
245
|
+
{ kind: "spawnSync", command: "git", args: ["-C", cwd, "remote", "get-url", "origin"] },
|
|
246
|
+
{ kind: "spawnSync", command: "git", args: ["ls-remote", "--refs", "https://example.invalid/repo.git", "refs/heads/main"] },
|
|
247
|
+
]);
|
|
248
|
+
assert.deepEqual(remoteHead, { advanced: false, remote_head: "c".repeat(40) });
|
|
249
|
+
const ci = await trace(captures, traces, "publication GH CI", () => publication.recheckReleaseFastPathCiStatusV1({ repositoryCwd: cwd, sha: "f".repeat(40), expectedStatus: "success" }), [
|
|
250
|
+
{ kind: "spawnSync", command: "gh", args: ["api", "repos/{owner}/{repo}/commits/ffffffffffffffffffffffffffffffffffffffff/check-runs?per_page=100", "--jq", "{total_count, returned: (.check_runs | length), checks: [.check_runs[] | [.status, .conclusion]]}"] },
|
|
251
|
+
{ kind: "spawnSync", command: "gh", args: ["api", "repos/{owner}/{repo}/commits/ffffffffffffffffffffffffffffffffffffffff/status", "--jq", ".state"] },
|
|
252
|
+
]);
|
|
253
|
+
assert.deepEqual(ci, { proven: false, status: null });
|
|
254
|
+
return { captures, traces };
|
|
255
|
+
} finally {
|
|
256
|
+
cp.execFileSync = original.execFileSync;
|
|
257
|
+
cp.execFile = original.execFile;
|
|
258
|
+
cp.spawnSync = original.spawnSync;
|
|
259
|
+
syncBuiltinESMExports();
|
|
260
|
+
t.after(() => {
|
|
261
|
+
cp.execFileSync = original.execFileSync;
|
|
262
|
+
cp.execFile = original.execFile;
|
|
263
|
+
cp.spawnSync = original.spawnSync;
|
|
264
|
+
syncBuiltinESMExports();
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
test("owned public adapters pass windowsHide at every mapped Node child-process boundary", async (t) => {
|
|
270
|
+
const { traces } = await captureOwnedRoutes(t);
|
|
271
|
+
assert.deepEqual(traces.map(({ label }) => label), [
|
|
272
|
+
"CodeGraph init",
|
|
273
|
+
"gentle_review inspect",
|
|
274
|
+
"repository authority",
|
|
275
|
+
"snapshot untracked discovery",
|
|
276
|
+
"transaction store root",
|
|
277
|
+
"transaction intended-commit gate",
|
|
278
|
+
"publication configured destination",
|
|
279
|
+
"publication remote ref",
|
|
280
|
+
"publication destination ref",
|
|
281
|
+
"publication advertised object",
|
|
282
|
+
"publication remote head",
|
|
283
|
+
"publication GH CI",
|
|
284
|
+
], "all twelve mapped public actions must complete before flag assertions");
|
|
285
|
+
for (const trace of traces) {
|
|
286
|
+
for (const capture of trace.captures) {
|
|
287
|
+
assert.equal(capture.options.windowsHide, true, `${trace.label}: ${capture.kind} ${capture.command} must hide Windows consoles`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("external editor remains the explicit interactive exemption", async () => {
|
|
293
|
+
const shell = await import("../extensions/gentle-shell.ts");
|
|
294
|
+
let received: Options | undefined;
|
|
295
|
+
const host = { stop() {}, start() {}, requestRender() {} };
|
|
296
|
+
assert.equal(shell.openInExternalEditor(host, "editor.txt", { EDITOR: "fixture-editor" }, ((_command: string, _args: readonly string[], options: Options) => {
|
|
297
|
+
received = options;
|
|
298
|
+
return { status: 0 };
|
|
299
|
+
}) as never, process.cwd()), true);
|
|
300
|
+
assert.equal(received?.shell, process.platform === "win32");
|
|
301
|
+
assert.equal(received?.stdio, "inherit");
|
|
302
|
+
assert.equal(received?.windowsHide, undefined, "the interactive editor intentionally remains unhidden");
|
|
303
|
+
});
|