Files
gstack/test/setup-codex-model.test.ts
T
Garry Tanandcapy-ai[bot] ef99eb5a55 feat: retire /investigate after a no-gain eval (GST-1717) (#4110)
* feat: retire /investigate after a no-gain eval (GST-1717)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(skill-start): core routing offer drops every non-core rule line (GST-1717)

The retired /investigate rule became 'Bugs/errors → fix them directly', which no
longer matches '→ invoke /', so proactive=core leaked it into the offered section.
Match any '- … → ' rule line.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* revert(review): keep review/checklist.md at its no-loss-pinned bytes (GST-1717)

The pin names replay GSTA-224 H1; a reviewer checklist edit merges only after a
passing no-loss replay (crew rule 15). The /investigate flake line stays until a
replayed repin.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: repair free-suite fallout from retiring /investigate (GST-1717)

CI run 42340956269 at 83a8a9a: drop fixtures, ledger rows, allowlist entries and
counts for the retired skill and its deleted paid files; record the harness
change as non-behavioral; the test-of-test detector accepts path.join(ROOT, 'bin');
uncovered-prompt fixtures use a tracked prompt, since an untracked path now
reads as a deleted one.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(relink): back up a retired skill's real SKILL.md before cleaning it (GST-1717)

With the skill's source gone, the customized-file check never ran, so a
banner-carrying local copy of investigate/SKILL.md was deleted instead of moved
to the backup root (red-team review, pass 1).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(qm): restore the review checklist's flake-line rewrite (GST-1717)

review/checklist.md keeps its pinned /investigate line, so the QM edition needs
its replaceText rule back; qm/ stays fresh against the generator.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): name the setup flag break, the relink backup and the pinned checklist line (GST-1717)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup,relink): back up a retired skill's real SKILL.md instead of deleting it (GST-1717)

Scoped fix round under grant a-360dd62bd46a (outside-voice blocking finding and
adversarial #1, #4). setup's prune compares a copied host entry with the render
before removing that render, and backs up a copy that differs or whose render is
gone; relink no longer takes the marker's rm -rf for a skill whose source is
gone, so a real SKILL.md is backed up and the user's files stay. setup reports a
failed remove-source of the retired gate hooks on stderr.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pr-profile): a changed prompt counts as deleted only when it is gone from disk (GST-1717)

An untracked prompt file on disk still needs prompt validation (targeted review
finding); harness change recorded as non-behavioral.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): exact backup wording, the hook-removal warning and GST-1744 (GST-1717)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(setup): one backup folder per host for a retired skill's copies (GST-1717)

Under grant a-c95576b8b1f9 (fix-check#1): every copy host moved its
gstack-investigate/SKILL.md to the same backup path, so a later host's copy
overwrote an earlier host's edits. A sandboxed ./setup --host factory run showed
gen-skill-docs removes the retired render before the prune, so every real copy
is backed up; each host now gets its own folder.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): say exactly what setup backs up for a retired skill (GST-1717)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): end the two colon sentences prose-lint flagged (GST-1717)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(retired-skills): name plan-eng-review once behind a pregate-literal waiver (GST-1717)

pregate's literals check read the GSTACK_RETIRED_SKILLS change as an old
value still in the tests. plan-eng-review is still retired; investigate was
appended. The fixture names it once, with the waiver.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-07 06:52:04 +00:00

108 lines
5.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
describe('setup Codex model activation', () => {
test('exposes --model and limits it to Codex installs', () => {
expect(setup).toContain('--model <id>');
expect(setup).toContain('MODEL_OVERRIDE_SET=1');
expect(setup).toContain('--model is supported only when Codex is selected');
// The override reaches the resolver as QUOTED argv — an unquoted
// regression would word-split/glob user input.
expect(setup).toContain('--explicit "$MODEL_OVERRIDE"');
});
test('resolver runs on every setup, before any INSTALL_CODEX gate', () => {
// A plain `./setup` (claude host) still regenerates .agents/, and live
// ~/.codex/skills symlinks point into it — resolution must not be gated
// on the codex host being selected, or a Sol user's profile gets
// clobbered back to the hardcoded fallback.
const blockStart = setup.indexOf('# Resolve the model overlay');
const blockEnd = setup.indexOf('# 1. Build browse binary', blockStart);
expect(blockStart).toBeGreaterThan(-1);
const block = setup.slice(blockStart, blockEnd);
const resolverAt = block.indexOf('_CODEX_MODEL_OUTPUT=');
const firstGateAt = block.indexOf('INSTALL_CODEX');
expect(resolverAt).toBeGreaterThan(-1);
expect(firstGateAt === -1 || resolverAt < firstGateAt).toBe(true);
});
test('resolves the profile once, fails closed, and passes it as quoted argv', () => {
expect(setup).toContain('scripts/resolve-codex-generation-model.ts');
expect(setup).toContain('CODEX_GENERATION_MODEL="gpt-6-astra"');
expect(setup).toContain('Codex skill profile: $CODEX_GENERATION_MODEL');
expect(setup).toContain('Source: $CODEX_GENERATION_MODEL_SOURCE');
expect(setup).toContain('gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL"');
// Positive pin of the parse mechanism (an eval-shaped regression would
// remove this line rather than merely rephrase an eval call).
expect(setup).toContain(`IFS=$'\\t' read -r CODEX_GENERATION_MODEL CODEX_GENERATION_MODEL_SOURCE`);
// Fail-closed: empty resolver output aborts setup, including the exit.
const guardAt = setup.indexOf('gstack setup failed: Codex model resolver returned no model');
expect(guardAt).toBeGreaterThan(-1);
expect(setup.slice(guardAt, guardAt + 200)).toContain('exit 1');
});
test('regenerates Codex after both fresh and stale build paths', () => {
const generationStart = setup.indexOf('# 1b. Generate .agents/ Codex skill docs');
const generationEnd = setup.indexOf('# 1c. Generate .factory/', generationStart);
const block = setup.slice(generationStart, generationEnd);
expect(block).toContain('if [ "$NEEDS_AGENTS_GEN" -eq 1 ]; then');
expect(block).not.toContain('NEEDS_BUILD" -eq 0');
});
test('fallback generation and handoff preserve the selected profile', () => {
const linkStart = setup.indexOf('link_codex_skill_dirs()');
const linkEnd = setup.indexOf('create_agents_sidecar()', linkStart);
const block = setup.slice(linkStart, linkEnd);
expect(block).toContain('gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL"');
expect(block).toContain('gen:skill-docs --host codex --model $CODEX_GENERATION_MODEL');
expect(setup).toContain('model changes: rerun ./setup --host codex');
expect(setup).toContain('model profile: $CODEX_GENERATION_MODEL');
});
test('Kiro uses its own host render without rewriting the Codex profile', () => {
const kiroStart = setup.indexOf('# 6. Install for Kiro CLI');
const kiroEnd = setup.indexOf('# 6b.', kiroStart);
expect(kiroStart).toBeGreaterThan(-1);
const block = setup.slice(kiroStart, kiroEnd);
expect(block).toContain('gen:skill-docs --host kiro');
expect(block).toContain('KIRO_DIR="$SOURCE_GSTACK_DIR/.kiro/skills"');
expect(block).not.toContain('gen:skill-docs --host codex');
expect(block).not.toContain('CODEX_GENERATION_MODEL');
});
test('Kiro installs its native root, upgrade skill, and bin/lib runtime assets', () => {
const kiroStart = setup.indexOf('# 6. Install for Kiro CLI');
const kiroEnd = setup.indexOf('# 6b.', kiroStart);
const block = setup.slice(kiroStart, kiroEnd);
expect(block).toContain('_copy_skill_md "$KIRO_DIR/gstack-upgrade/SKILL.md"');
expect(block).toContain('_copy_skill_md "$KIRO_DIR/gstack/SKILL.md"');
expect(block).toContain('_link_or_copy "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin"');
expect(block).toContain('_link_or_copy "$SOURCE_GSTACK_DIR/lib" "$KIRO_GSTACK/lib"');
expect(block).not.toContain('$AGENTS_DIR');
});
test('Codex skills path honors CODEX_HOME', () => {
expect(setup).toContain('CODEX_SKILLS="${CODEX_HOME:-$HOME/.codex}/skills"');
});
test('--model prints the one-shot persistence note', () => {
expect(setup).toContain('--model applies to this run only');
});
});
describe('Codex E2E hermetic model pin', () => {
const runner = fs.readFileSync(path.join(ROOT, 'test', 'helpers', 'codex-session-runner.ts'), 'utf8');
test('copies authentication only and can ignore operator config', () => {
expect(runner).toContain("for (const entry of ['auth.json'])");
expect(runner).toContain("if (ignoreUserConfig) args.push('--ignore-user-config')");
expect(runner).toContain('CODEX_HOME: tempCodexDir');
expect(runner).not.toContain("if (entry === 'skills') continue");
});
});