mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 01:45:29 +02:00
* feat(cso): add verified audits and replayable repair bundles * fix(cso): harden qualification and setup boundaries * fix(cso): assemble security canaries at runtime * fix(cso): bound release proof and maintenance work Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): require complete evaluation reports Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): replay expired snapshots from supplied source Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): synchronize DNS cancellation assertion Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore(ship): exempt repository owner from liveness proof Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): make recheck retention overlap deterministic Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: bump version and changelog (v1.85.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass native release gates Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.86.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): resolve rechecks by finding Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.87.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass macOS and Windows release gates Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): harden native verification gates * fix(cso): refine Windows native diagnostics * test(cso): isolate Windows Git startup failure * test(cso): stabilize Windows native diagnostics * fix(cso): support hardened Git on Windows * fix(cso): close final verification gaps * test(cso): bound cold Docker fixture setup * fix(cso): restore cross-platform free-suite gates --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
168 lines
10 KiB
TypeScript
168 lines
10 KiB
TypeScript
/** Full setup ordering: repair another installed host before build/link/prune. */
|
|
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const BANNER = '<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->';
|
|
const oldSkill = (name: string) => `---\nname: ${name}\n---\n${BANNER}\nold working installation\n`;
|
|
function put(file: string, text: string, mode = 0o644) {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, text, { mode });
|
|
}
|
|
|
|
function fixture(copy: boolean) {
|
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-setup-rename-'));
|
|
const root = path.join(temp, 'checkout');
|
|
const home = path.join(temp, 'home');
|
|
const codexHome = path.join(home, '.codex');
|
|
const skills = path.join(codexHome, 'skills');
|
|
const events = path.join(temp, 'events.log');
|
|
const shims = path.join(temp, 'shims');
|
|
// Real setup, generator, resolver and migration code; only dependency
|
|
// installation and binary compilation are stubbed to keep this test free.
|
|
for (const dir of ['scripts', 'hosts', 'lib', 'model-overlays', 'browse/src', 'design/src', 'openclaw/templates']) {
|
|
fs.cpSync(path.join(ROOT, dir), path.join(root, dir), { recursive: true });
|
|
}
|
|
fs.copyFileSync(path.join(ROOT, 'setup'), path.join(root, 'setup'));
|
|
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']) {
|
|
const target = path.join(root, 'bin', bin);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.copyFileSync(path.join(ROOT, 'bin', bin), target);
|
|
}
|
|
// Include the required versioned hook too: it runs after host linking and
|
|
// must be idempotent with the early setup repair, including failed retries.
|
|
const migrationDir = path.join(ROOT, 'gstack-upgrade/migrations');
|
|
for (const file of fs.readdirSync(migrationDir).filter(name => name.endsWith('.sh'))) {
|
|
const content = fs.readFileSync(path.join(migrationDir, file), 'utf8');
|
|
if (content.includes('gstack-migrate-claude-code')) put(path.join(root, 'gstack-upgrade/migrations', file), content, 0o755);
|
|
}
|
|
put(path.join(home, '.gstack/.last-setup-version'), '1.0.0.0\n');
|
|
// Limit the fixture's skills, retaining real resolver calls and real native
|
|
// lazy sections without copying unrelated skills' runtime data.
|
|
put(path.join(root, 'SKILL.md.tmpl'), '---\nname: gstack\ndescription: Fixture router.\n---\n{{OUTSIDE_VOICE_ROUTING}}\n');
|
|
for (const skill of ['review', 'claude-code', 'codex']) {
|
|
fs.mkdirSync(path.join(root, skill), { recursive: true });
|
|
fs.copyFileSync(path.join(ROOT, skill, 'SKILL.md.tmpl'), path.join(root, skill, 'SKILL.md.tmpl'));
|
|
const sections = path.join(ROOT, skill, 'sections');
|
|
if (fs.existsSync(sections)) {
|
|
for (const file of fs.readdirSync(sections).filter(name => name.endsWith('.tmpl') || name === 'manifest.json')) {
|
|
put(path.join(root, skill, 'sections', file), fs.readFileSync(path.join(sections, file), 'utf8'));
|
|
}
|
|
}
|
|
}
|
|
put(path.join(root, 'open-gstack-browser/SKILL.md.tmpl'), '---\nname: open-gstack-browser\ndescription: Fixture browser.\n---\nBrowser fixture.\n');
|
|
// Observe actual isolated/live generation and inject only an isolated
|
|
// generation failure. Later setup generation still runs the real generator.
|
|
const generator = path.join(root, 'scripts/gen-skill-docs.ts');
|
|
const source = fs.readFileSync(generator, 'utf8').replace(/^#![^\n]*\n/, '');
|
|
fs.writeFileSync(generator, `const fixtureIsolated = process.argv.includes('--out-dir');
|
|
(await import('node:fs')).appendFileSync(process.env.FIXTURE_EVENTS!, 'generate:' + (fixtureIsolated ? 'isolated' : 'live') + ':defer=' + (process.env.GSTACK_DEFER_CLAUDE_RENAME_PRUNE ?? '0') + '\\n');
|
|
if (fixtureIsolated && process.env.FIXTURE_RENAME_FAIL === '1') { console.error('fixture isolated generation failure'); process.exit(70); }
|
|
${source}`);
|
|
put(path.join(shims, 'bun'), `#!/usr/bin/env bash
|
|
set -e
|
|
case "$1" in
|
|
install) exit 0 ;;
|
|
esac
|
|
if [ "$1" = run ] && [ "$2" = build ]; then
|
|
printf 'build\\n' >> "$FIXTURE_EVENTS"
|
|
"$FIXTURE_REAL_BUN" run scripts/gen-skill-docs.ts --host all
|
|
for target in browse/dist/browse design/dist/design make-pdf/dist/pdf bin/gstack-cso-core bin/gstack-cso-launcher bin/gstack-cso-watchdog; do
|
|
mkdir -p "$(dirname "$target")"
|
|
printf '#!/usr/bin/env bash\\nexit 0\\n' > "$target"
|
|
chmod +x "$target"
|
|
done
|
|
printf '%064d\\n' 0 > bin/.gstack-cso-generation
|
|
printf 'complete\\n' > browse/dist/.build-complete
|
|
exit 0
|
|
fi
|
|
if [ "$1" = run ] && [ "$2" = gen:skill-docs ]; then
|
|
shift 2
|
|
exec "$FIXTURE_REAL_BUN" run scripts/gen-skill-docs.ts "$@"
|
|
fi
|
|
exec "$FIXTURE_REAL_BUN" "$@"
|
|
`, 0o755);
|
|
const oldRender = path.join(root, '.agents/skills/gstack-claude');
|
|
put(path.join(oldRender, 'SKILL.md'), oldSkill('gstack-claude'));
|
|
fs.mkdirSync(skills, { recursive: true });
|
|
if (copy) put(path.join(skills, 'gstack-claude/SKILL.md'), oldSkill('gstack-claude'));
|
|
else fs.symlinkSync(oldRender, path.join(skills, 'gstack-claude'));
|
|
put(path.join(skills, 'gstack-review/SKILL.md'), oldSkill('gstack-review'));
|
|
put(path.join(skills, 'gstack-review/notes.md'), 'user notes\n');
|
|
put(path.join(codexHome, 'config.toml'), 'model = "gpt-5.6-sol"\n');
|
|
const env = { ...process.env, HOME: home, CODEX_HOME: codexHome,
|
|
GSTACK_HOME: path.join(home, '.gstack'), GSTACK_STATE_ROOT: path.join(home, '.gstack'),
|
|
GSTACK_SKIP_PLAYWRIGHT: '1', GSTACK_SKIP_FONTS: '1', GSTACK_SKIP_COREUTILS: '1', GSTACK_SKIP_ASIDE: '1',
|
|
GSTACK_SKIP_GBRAIN_REGEN: '1', GSTACK_PLAN_TUNE_HOOKS: 'no', GSTACK_TIMELINE_STOP_HOOK: 'no',
|
|
PATH: `${shims}${path.delimiter}${process.env.PATH}`, FIXTURE_REAL_BUN: process.execPath, FIXTURE_EVENTS: events,
|
|
};
|
|
const run = (failure = false) => spawnSync('bash', [path.join(root, 'setup'), '--host', 'claude', '--no-prefix', '--no-team'], {
|
|
cwd: root, env: { ...env, FIXTURE_RENAME_FAIL: failure ? '1' : '0' }, encoding: 'utf8', timeout: 30_000,
|
|
});
|
|
return { temp, root, home, skills, events, oldRender, run };
|
|
}
|
|
|
|
describe.skipIf(process.platform === 'win32')('full setup repairs the installed Claude wrapper through another host', () => {
|
|
for (const copy of [false, true]) {
|
|
test(`Claude-only setup migrates existing Codex, survives later generation/linking, and repeats safely (copy=${copy})`, () => {
|
|
const f = fixture(copy);
|
|
try {
|
|
// The real generator runs before the stubbed dependency-install step.
|
|
// Exercise that ordering with no node_modules in the source checkout.
|
|
expect(fs.existsSync(path.join(f.root, 'node_modules'))).toBe(false);
|
|
const result = f.run();
|
|
expect({ code: result.status, stderr: result.stderr }).toMatchObject({ code: 0 });
|
|
expect(result.stdout).toContain('gstack ready (claude)');
|
|
expect(result.stderr).toContain('/claude is now /claude-code');
|
|
const next = path.join(f.skills, 'gstack-claude-code/SKILL.md');
|
|
expect(fs.readFileSync(next, 'utf8')).toContain('name: claude-code');
|
|
expect(fs.existsSync(path.join(f.skills, 'gstack-claude/SKILL.md'))).toBe(false);
|
|
expect(fs.existsSync(f.oldRender)).toBe(false);
|
|
expect(fs.readFileSync(path.join(f.skills, 'gstack-review/SKILL.md'), 'utf8')).toContain('gstack-claude-code');
|
|
expect(fs.readFileSync(path.join(f.skills, 'gstack-review/notes.md'), 'utf8')).toBe('user notes\n');
|
|
expect(fs.readFileSync(path.join(f.root, '.agents/skills/gstack-review/SKILL.md'), 'utf8')).toContain('Model-Specific Behavioral Patch (gpt-5.6-sol)');
|
|
expect(fs.existsSync(path.join(f.skills, 'gstack/bin/gstack-claude-code'))).toBe(true);
|
|
expect(fs.existsSync(path.join(f.skills, 'gstack/lib/outside-review-result.ts'))).toBe(true);
|
|
const fakeCli = path.join(f.temp, 'fake-reviewer.ts');
|
|
put(fakeCli, "await Bun.stdin.text(); console.log(JSON.stringify({result:'NO_FINDINGS',session_id:'installed-session'}));\n");
|
|
const installed = spawnSync(process.execPath, [path.join(f.skills, 'gstack/bin/gstack-claude-code'),
|
|
'--cwd', f.root, '--access', 'none', '--timeout-ms', '3000'], {
|
|
env: { ...process.env, GSTACK_CLAUDE_BIN: process.execPath, GSTACK_CLAUDE_BIN_ARGS: JSON.stringify([fakeCli]) },
|
|
input: 'Review through the migrated runtime.', encoding: 'utf8', timeout: 5000,
|
|
});
|
|
expect({ code: installed.status, stderr: installed.stderr }).toMatchObject({ code: 0 });
|
|
expect(JSON.parse(installed.stdout)).toMatchObject({ status: 'completed', provider: 'claude-code', session_id: 'installed-session' });
|
|
expect(fs.existsSync(path.join(f.home, '.claude/skills/review/SKILL.md'))).toBe(true);
|
|
expect(fs.existsSync(path.join(f.home, '.claude/skills/claude-code/SKILL.md'))).toBe(false);
|
|
expect(fs.readFileSync(f.events, 'utf8')).toMatch(/generate:isolated:defer=1\nbuild\ngenerate:live:defer=0/);
|
|
const repeated = f.run();
|
|
expect(repeated.status).toBe(0);
|
|
expect(repeated.stderr).not.toContain('/claude is now');
|
|
expect(fs.readFileSync(next, 'utf8')).toContain('name: claude-code');
|
|
} finally { fs.rmSync(f.temp, { recursive: true, force: true }); }
|
|
}, 60_000);
|
|
}
|
|
|
|
test('failed rename keeps the working Codex entry through later build/prune and succeeds on retry', () => {
|
|
const f = fixture(false);
|
|
try {
|
|
const failed = f.run(true);
|
|
expect({ code: failed.status, stderr: failed.stderr }).toMatchObject({ code: 0 });
|
|
expect(failed.stdout).toContain('gstack ready (claude)');
|
|
expect(failed.stderr).toContain('fixture isolated generation failure');
|
|
expect(fs.readFileSync(path.join(f.skills, 'gstack-claude/SKILL.md'), 'utf8')).toContain('old working installation');
|
|
expect(fs.readFileSync(f.events, 'utf8')).toMatch(/generate:isolated:defer=1\nbuild\ngenerate:live:defer=1/);
|
|
const retry = f.run();
|
|
expect(retry.status).toBe(0);
|
|
expect(fs.existsSync(path.join(f.skills, 'gstack-claude/SKILL.md'))).toBe(false);
|
|
expect(fs.readFileSync(path.join(f.skills, 'gstack-claude-code/SKILL.md'), 'utf8')).toContain('name: claude-code');
|
|
} finally { fs.rmSync(f.temp, { recursive: true, force: true }); }
|
|
}, 60_000);
|
|
});
|