mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
feat: finalize configurable model tiers and verified release repairs
This commit is contained in:
1 parent
b4ba71b28d
commit
f823ca5c49
13 files changed
+158
-60
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.35.0] - 2026-10-07
|
||||
|
||||
### Added
|
||||
- Choose separate model tiers for planning and implementation. Independent plan reviews default to Fable 5.1 or GPT-6 Astra; implementation handoffs recommend Opus 5.5 or GPT-6.1 Sol without switching your session. Explicit model choices still win, and `plan_review_tier smart` or `host` provides an alternative to the frontier default.
|
||||
- Inspect effective models, their sources and copyable pin/reset commands with `gstack-models`. Six settings use the existing configuration store, with fail-closed validation and a notice before the first affected review. See the [model-policy guide](docs/model-policy.md).
|
||||
- Get weekly advisory checks of official model recommendations and retirement notices. One tracking issue retains source evidence and lifecycle history; upgrades remain human-reviewed, with no automatic model changes or paid benchmarking.
|
||||
|
||||
### Fixed
|
||||
- Codex model probes retain up to sixteen selections, serialize concurrent misses and preserve newer entries when another probe finishes later. Plan-review readiness and execution share one deadline, and failed model choices point to the setting that actually selected them.
|
||||
- `/review` and `/ship` explicitly require prerequisite QA instructions to be read in earlier responses before a probe, including its evidence capture.
|
||||
|
||||
## [1.91.34.0] - 2026-10-07
|
||||
|
||||
**/autoplan works again on current Claude Code, including long sessions and `claude --bg`.**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.34.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.35.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+6
-5
@@ -851,11 +851,12 @@ If token count is not available, display: `Tokens: unknown`
|
||||
None of these is an auth or network failure, and the auth probe cannot catch them.
|
||||
Recovery, in order:
|
||||
1. Read the `CODEX_MODEL:` line: it names the model and where it came from.
|
||||
2. Fix that source: name another model for this request, update
|
||||
`GSTACK_CODEX_MODEL`, or change `model` (or `review_model`) in the Codex
|
||||
`config.toml`. With none of these set, gstack uses `gpt-6-astra`; set any of
|
||||
them to a model the account can use.
|
||||
3. If Codex printed `[notice.model_migrations]`, use that replacement model.
|
||||
2. Fix that source and relay its HINT/Repair lines. With `--role plan-review`,
|
||||
tier pins outrank native settings; native `model`/`review_model` applies
|
||||
only in `host` mode. Without a role, request, `GSTACK_CODEX_MODEL`, then
|
||||
native settings choose the model, falling back to `gpt-6-astra`.
|
||||
3. If Codex printed `[notice.model_migrations]`, use that replacement for
|
||||
no-role calls. For a role, offer an explicit override; do not silently substitute.
|
||||
Never present this as a model stall or a PASS — it is a fail-closed gate result.
|
||||
- **`VERDICT: unavailable`:** the shared validator found the run did not execute (for
|
||||
example `Codex's sandbox could not start here`). Relay its line and fix verbatim; it is
|
||||
|
||||
+6
-5
@@ -317,11 +317,12 @@ If token count is not available, display: `Tokens: unknown`
|
||||
None of these is an auth or network failure, and the auth probe cannot catch them.
|
||||
Recovery, in order:
|
||||
1. Read the `CODEX_MODEL:` line: it names the model and where it came from.
|
||||
2. Fix that source: name another model for this request, update
|
||||
`GSTACK_CODEX_MODEL`, or change `model` (or `review_model`) in the Codex
|
||||
`config.toml`. With none of these set, gstack uses `gpt-6-astra`; set any of
|
||||
them to a model the account can use.
|
||||
3. If Codex printed `[notice.model_migrations]`, use that replacement model.
|
||||
2. Fix that source and relay its HINT/Repair lines. With `--role plan-review`,
|
||||
tier pins outrank native settings; native `model`/`review_model` applies
|
||||
only in `host` mode. Without a role, request, `GSTACK_CODEX_MODEL`, then
|
||||
native settings choose the model, falling back to `gpt-6-astra`.
|
||||
3. If Codex printed `[notice.model_migrations]`, use that replacement for
|
||||
no-role calls. For a role, offer an explicit override; do not silently substitute.
|
||||
Never present this as a model stall or a PASS — it is a fail-closed gate result.
|
||||
- **`VERDICT: unavailable`:** the shared validator found the run did not execute (for
|
||||
example `Codex's sandbox could not start here`). Relay its line and fix verbatim; it is
|
||||
|
||||
+9
-5
@@ -1148,10 +1148,10 @@ This is my **second opinion mode**.
|
||||
|
||||
`/codex` brings OpenAI Codex CLI to review the same diff independently. It is available on every harness except Codex itself. External harnesses install it as `/gstack-codex`. Compare its findings with the native review to distinguish corroborated findings from issues only one reviewer caught.
|
||||
|
||||
gstack-owned Codex calls default to `gpt-6-astra`, including resumed consult
|
||||
sessions. Set `GSTACK_CODEX_MODEL=<model>` to change the default, or name a
|
||||
model in your request to override it for that invocation. Generated commands
|
||||
pass the selection through `-c model=...`, overriding the CLI's configured model.
|
||||
Without a role, gstack-owned Codex calls select a request-specific model, then
|
||||
`GSTACK_CODEX_MODEL`, then Codex native settings, falling back to `gpt-6-astra`.
|
||||
This also applies to resumed consultations. Generated commands pass the selection
|
||||
through `-c model=...`, overriding the CLI's configured model for that call.
|
||||
Native review also sets `-c review_model=...` to that selection, overriding any
|
||||
separate review-model pin.
|
||||
|
||||
@@ -1161,6 +1161,10 @@ review, challenge, and consult calls preserve Claude's configured model.
|
||||
sessions; a model named in your request takes precedence. Harness routing is
|
||||
independent of model selection.
|
||||
|
||||
An explicit `--role plan-review` uses the configured plan-review tier instead;
|
||||
request and environment overrides still win, and `host` mode restores native
|
||||
selection. See [model policy](model-policy.md) for pins, precedence and recovery.
|
||||
|
||||
### Three modes
|
||||
|
||||
**Review** — run `codex review` against the current diff. Codex reads every changed file, classifies findings by severity (P1 critical, P2 high, P3 medium), and returns a PASS/FAIL verdict. Any P1 finding = FAIL. The review is fully independent — Codex doesn't see Claude's review.
|
||||
@@ -1200,7 +1204,7 @@ Claude Code provides the outside reviewer when gstack runs in Codex. Other non-C
|
||||
|
||||
**Review** supplies the branch diff for a read-only pass/fail review. **Challenge** asks Claude Code to find concrete failure cases in the same diff. **Consult** supports read-only repository exploration and resumes the session saved in `.context/claude-session-id`. Review and challenge receive context from the parent workflow and run without tools; consultation can read and search files.
|
||||
|
||||
The Claude Code CLI must be installed and authenticated. Its existing model configuration and `GSTACK_CLAUDE_BIN` / `GSTACK_CLAUDE_BIN_ARGS` executable overrides are honored. Errors, timeouts, and invalid responses report missing outside coverage instead of a clean review. Automatic reviews start fresh; consult session continuity is explicit.
|
||||
The Claude Code CLI must be installed and authenticated. Without a role, its existing model configuration is honored; explicit `--role plan-review` follows the [model policy](model-policy.md). `GSTACK_CLAUDE_BIN` / `GSTACK_CLAUDE_BIN_ARGS` executable overrides remain in force. Errors, timeouts, and invalid responses report missing outside coverage instead of a clean review. Automatic reviews start fresh; consult session continuity is explicit.
|
||||
|
||||
Outside-review routing follows the harness, independently of the configured model. Generic second-opinion requests choose `/claude-code` on Codex and `/codex` elsewhere; explicit provider requests keep that provider. The existing `codex_reviews` setting controls the selected automatic reviewer in workflows that already use that setting. Existing opt-in and skip controls still apply in office hours, design, and spec workflows.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.34",
|
||||
"version": "1.91.35",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"patterns": [
|
||||
"test/helpers/agent-sdk-runner.ts",
|
||||
"test/helpers/codex-session-runner.ts",
|
||||
@@ -10,44 +10,44 @@
|
||||
"test/helpers/llm-judge.ts"
|
||||
],
|
||||
"files": {
|
||||
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":2},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":2},
|
||||
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":2},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":2},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":2},
|
||||
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":2},
|
||||
"scripts/test-pr-profile.ts": {"blob":"88ecbce2b71c1e388c4dbca3f7013c71013bf8d3","since":2,"nonBehavioral":"test-pr-profile classifies the dispatch-only measure-journal-read workflow as a free-lane path; selection of paid cases and every verdict are unchanged"},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":2},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":2},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":2},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":2},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":2},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":2},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":2},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":2},
|
||||
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":2},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":2},
|
||||
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":2},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":2},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":2},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":2},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":2},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":2},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":2},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":2},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":2},
|
||||
"test/helpers/pty/launch.ts": {"blob":"4ef0bcb7b5e7c35d2aac0cbffd7f8a303440b6fe","since":2},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":2},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":2},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"465f01641fb3a18ae1c9d2613a9c2b26f8fb503e","since":2},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":2},
|
||||
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":2},
|
||||
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":2},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":2},
|
||||
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":2},
|
||||
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":2},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":2},
|
||||
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":2},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":2}
|
||||
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":3},
|
||||
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":3},
|
||||
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":3},
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":3},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":3},
|
||||
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":3},
|
||||
"scripts/test-pr-profile.ts": {"blob":"54363c3d3aad9743b9e79219da7ec8a0f4ab0832","since":3},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":3},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":3},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":3},
|
||||
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":3},
|
||||
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":3},
|
||||
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":3},
|
||||
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":3},
|
||||
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":3},
|
||||
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":3},
|
||||
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":3},
|
||||
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":3},
|
||||
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":3},
|
||||
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":3},
|
||||
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":3},
|
||||
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":3},
|
||||
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":3},
|
||||
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":3},
|
||||
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":3},
|
||||
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":3},
|
||||
"test/helpers/pty/launch.ts": {"blob":"ca874247a5b6bf6076d3cd34da01af9c8cc292b7","since":3},
|
||||
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":3},
|
||||
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":3},
|
||||
"test/helpers/pty/runners/floor.ts": {"blob":"465f01641fb3a18ae1c9d2613a9c2b26f8fb503e","since":3},
|
||||
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":3},
|
||||
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":3},
|
||||
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":3},
|
||||
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":3},
|
||||
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":3},
|
||||
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":3},
|
||||
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":3},
|
||||
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":3},
|
||||
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":3}
|
||||
}
|
||||
}
|
||||
@@ -947,7 +947,7 @@ describe('codex broken-install detection (#2742)', () => {
|
||||
expect(src).toContain('echo "CODEX_MODE: $_CODEX_MODE"');
|
||||
}
|
||||
const role = fs.readFileSync(PROBE, 'utf-8').match(/_gstack_codex_role_ready\(\) \{[\s\S]*?\n\}/)![0];
|
||||
expect(role.trim().split('\n').at(-2)!.trim()).toBe('_gstack_codex_model_probe "$1"');
|
||||
expect(role).toContain('_gstack_codex_model_probe "$1" || return $?');
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-role-broken-'));
|
||||
try {
|
||||
const bin = path.join(home, 'bin');
|
||||
|
||||
@@ -493,6 +493,7 @@ async function closePty(pty: PtyProcess, wallTimer: ReturnType<typeof setTimeout
|
||||
} finally {
|
||||
clearTimeout(cleanupTimer);
|
||||
clearTimeout(wallTimer);
|
||||
pty.proc.terminal?.close?.();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -552,6 +552,14 @@ describe('Claude plan-review invocation emits one selected model or none', () =>
|
||||
|
||||
describe('manual /codex entry: explicit role only, paid probe only for the dispatched model', () => {
|
||||
const skill = fs.readFileSync(path.join(ROOT, 'codex', 'SKILL.md.tmpl'), 'utf8');
|
||||
test('manual model recovery distinguishes role pins from native no-role settings', () => {
|
||||
const recovery = skill.slice(skill.indexOf('Recovery, in order:'), skill.indexOf('- **`VERDICT: unavailable`:'));
|
||||
expect(recovery).toContain('With `--role plan-review`');
|
||||
expect(recovery).toContain('tier pins outrank native settings');
|
||||
expect(recovery).toContain('only in `host` mode');
|
||||
expect(recovery).toContain('Without a role');
|
||||
expect(recovery).toContain('do not silently substitute');
|
||||
});
|
||||
const preflight = skill.match(/```bash\n(_CODEX_PROBE=[^\n]*\n_CODEX_ROLE=''[\s\S]*?)\n```/)![1]!
|
||||
.replace('{{OUTSIDE_SELF_GUARD:codex}}', RESOLVERS.OUTSIDE_SELF_GUARD(ctxFor('codex', 'claude', HOST_PATHS.claude), ['codex']));
|
||||
const withRole = (text: string, role: string) => text.replaceAll("_CODEX_ROLE=''", `_CODEX_ROLE='${role}'`);
|
||||
|
||||
@@ -4,6 +4,78 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { launchClaudePty, runPlanSkillCounting, type ClaudePtySession } from './helpers/claude-pty-runner';
|
||||
|
||||
test.each(['exited', 'running', 'unresponsive'])('closes the owned terminal once after %s child cleanup', async mode => {
|
||||
const originalBinary = process.env.BROWSE_TERMINAL_BINARY;
|
||||
const events: string[] = [];
|
||||
let resolveExit!: (code: number) => void;
|
||||
const exited = new Promise<number>(resolve => { resolveExit = resolve; });
|
||||
if (mode === 'exited') resolveExit(0);
|
||||
const spawn = Bun.spawn;
|
||||
const spawnSpy = spyOn(Bun, 'spawn').mockImplementation(((...args: any[]) =>
|
||||
args[1]?.env?.GSTACK_TEST_TERMINAL_CLEANUP === mode ? {
|
||||
exited,
|
||||
kill(signal: string) {
|
||||
events.push(signal);
|
||||
if (mode === 'running') resolveExit(0);
|
||||
},
|
||||
terminal: { write() {}, close() { events.push('close'); } },
|
||||
} : (spawn as any)(...args)) as typeof Bun.spawn);
|
||||
let session: ClaudePtySession | undefined;
|
||||
try {
|
||||
process.env.BROWSE_TERMINAL_BINARY = process.execPath;
|
||||
session = await launchClaudePty({ timeoutMs: 200, sessionLedger: false,
|
||||
env: { GSTACK_TEST_TERMINAL_CLEANUP: mode } });
|
||||
await Promise.all([session.close(), session.close()]);
|
||||
await session.close();
|
||||
expect(events.filter(event => event === 'close')).toHaveLength(1);
|
||||
expect(events.at(-1)).toBe('close');
|
||||
if (mode === 'exited') expect(events).toEqual(['close']);
|
||||
if (mode === 'running') expect(events).toEqual(['SIGINT', 'close']);
|
||||
if (mode === 'unresponsive') expect(events).toContain('SIGKILL');
|
||||
} finally {
|
||||
try { await session?.close(); }
|
||||
finally {
|
||||
spawnSpy.mockRestore();
|
||||
if (originalBinary === undefined) delete process.env.BROWSE_TERMINAL_BINARY;
|
||||
else process.env.BROWSE_TERMINAL_BINARY = originalBinary;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(process.platform !== 'linux')('closes the owned terminal without retaining native PTY descriptors', async () => {
|
||||
const descriptors = () => fs.readdirSync('/proc/self/fd').filter(fd => {
|
||||
try { return /\/dev\/(?:pts\/)?ptmx$/.test(fs.readlinkSync(`/proc/self/fd/${fd}`)); }
|
||||
catch { return false; }
|
||||
}).sort();
|
||||
const before = descriptors();
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-pty-close-'));
|
||||
const fake = path.join(dir, 'fake-claude');
|
||||
fs.writeFileSync(fake, `#!${process.execPath}
|
||||
process.on('SIGINT', () => process.exit(0));
|
||||
process.stdin.resume();
|
||||
process.stdout.write('TERMINAL_READY');
|
||||
`, { mode: 0o755 });
|
||||
const originalBinary = process.env.BROWSE_TERMINAL_BINARY;
|
||||
let session: ClaudePtySession | undefined;
|
||||
try {
|
||||
process.env.BROWSE_TERMINAL_BINARY = fake;
|
||||
session = await launchClaudePty({ cwd: dir, model: 'fixture', timeoutMs: 5000, sessionLedger: false });
|
||||
await session.waitFor('TERMINAL_READY', { timeoutMs: 3000 });
|
||||
expect(descriptors().length).toBeGreaterThan(before.length);
|
||||
await session.close();
|
||||
expect(session.exited()).toBe(true);
|
||||
expect(descriptors()).toEqual(before);
|
||||
} finally {
|
||||
try { await session?.close(); }
|
||||
finally {
|
||||
if (originalBinary === undefined) delete process.env.BROWSE_TERMINAL_BINARY;
|
||||
else process.env.BROWSE_TERMINAL_BINARY = originalBinary;
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
test.skipIf(process.platform === 'win32')('PTY output and exit wake observers without leaving deadline timers', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-pty-output-'));
|
||||
const fake = path.join(dir, 'fake-claude');
|
||||
|
||||
@@ -30,7 +30,7 @@ function fixture(copy: boolean) {
|
||||
put(path.join(root, 'VERSION'), '2.0.0.0\n');
|
||||
put(path.join(root, 'ETHOS.md'), 'Fixture ethos\n');
|
||||
put(path.join(root, 'package.json'), JSON.stringify({ type: 'module', scripts: { 'gen:skill-docs': 'bun run scripts/gen-skill-docs.ts' } }));
|
||||
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh']) {
|
||||
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-models', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh']) {
|
||||
const target = path.join(root, 'bin', bin);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.copyFileSync(path.join(ROOT, 'bin', bin), target);
|
||||
|
||||
Reference in new issue
Block a user