mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 07:59:02 +02:00
fix: resolve workflow gaps exposed by frontier evals
Clarify plan-review ordering and fallback modes, preserve deploy readiness gates, honor configured merge methods, correct benchmark and canary contracts, and restore vendored installs on setup failure. Cover recovery with real-shell regressions. Co-Authored-By: OpenAI Codex <noreply@openai.com>
This commit is contained in:
co-authored by
OpenAI Codex
parent
a9f9ec5f08
commit
1f678a5b81
@@ -471,7 +471,7 @@ export const CARVE_GUARDS: Record<string, CarveGuard> = {
|
||||
mustPrecedeStop: ['land-deploy-confirmed'],
|
||||
mustMoveToSection: [
|
||||
'PRE-MERGE READINESS REPORT',
|
||||
'gh pr merge --squash --auto --delete-branch',
|
||||
'gh pr merge "$MERGE_FLAG" --auto --delete-branch',
|
||||
'DEPLOY INFRASTRUCTURE VALIDATION',
|
||||
],
|
||||
gateAfterStop: undefined, // operational skill
|
||||
|
||||
@@ -85,9 +85,9 @@ describe("PR #1620 §4a-postfail in land-and-deploy template", () => {
|
||||
expect(body).toMatch(/Do NOT remove the user's primary working tree/);
|
||||
});
|
||||
|
||||
test("MERGED branch continues to §4a CI auto-deploy detection", () => {
|
||||
test("MERGED branch continues to §4b CI auto-deploy detection", () => {
|
||||
const body = readTmpl();
|
||||
expect(body).toMatch(/continue to §4a/);
|
||||
expect(body).toMatch(/continue to §4b \(CI auto-deploy detection\)/);
|
||||
});
|
||||
|
||||
// #2656: the failed merge carried --delete-branch; the recovery path must
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const template = readFileSync(join(import.meta.dir, '../gstack-upgrade/SKILL.md.tmpl'), 'utf8');
|
||||
const blockAfter = (marker: string) => {
|
||||
const section = template.slice(template.indexOf(marker));
|
||||
return section.match(/```bash\n([\s\S]*?)\n```/)![1].replaceAll('{{SETUP_COMMAND}}', './setup');
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('upgrade setup recovery (real shell)', () => {
|
||||
for (const mode of ['vendored', 'local'] as const) {
|
||||
for (const setupExit of [0, 1]) {
|
||||
test(`${mode}: setup exit ${setupExit} ${setupExit ? 'restores old install' : 'removes backup only after success'}`, () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'upgrade-recovery-'));
|
||||
const target = join(root, 'target');
|
||||
const source = join(root, 'source');
|
||||
const bin = join(root, 'bin');
|
||||
try {
|
||||
for (const dir of [target, source, bin]) mkdirSync(dir);
|
||||
writeFileSync(join(target, 'VERSION'), 'old');
|
||||
writeFileSync(join(source, 'VERSION'), 'new');
|
||||
writeFileSync(join(source, 'setup'), '#!/bin/sh\nexit "$SETUP_EXIT"\n', { mode: 0o755 });
|
||||
writeFileSync(join(bin, 'git'), '#!/bin/sh\nfor last; do :; done\ncp -R "$UPGRADE_FIXTURE" "$last"\n', { mode: 0o755 });
|
||||
const script = blockAfter(mode === 'vendored'
|
||||
? '**For vendored installs**'
|
||||
: '**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:**');
|
||||
const result = spawnSync('bash', ['-c', script], {
|
||||
cwd: root, encoding: 'utf8', timeout: 10_000,
|
||||
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, INSTALL_DIR: mode === 'vendored' ? target : source,
|
||||
LOCAL_GSTACK: target, UPGRADE_FIXTURE: source, SETUP_EXIT: String(setupExit) },
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(setupExit);
|
||||
expect(readFileSync(join(target, 'VERSION'), 'utf8')).toBe(setupExit ? 'old' : 'new');
|
||||
expect(existsSync(`${target}.bak`)).toBe(false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('git setup failure is not routed into the divergence reset fallback', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'upgrade-git-setup-'));
|
||||
try {
|
||||
const bin = join(root, 'bin');
|
||||
mkdirSync(bin);
|
||||
writeFileSync(join(bin, 'git'), '#!/bin/sh\nif [ "$1" = rev-parse ]; then echo old-commit; fi\nexit 0\n', { mode: 0o755 });
|
||||
writeFileSync(join(root, 'setup'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
||||
const result = spawnSync('bash', ['-c', blockAfter('**For git installs**')], {
|
||||
cwd: root, encoding: 'utf8', timeout: 10_000,
|
||||
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, INSTALL_DIR: root },
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain('SETUP_FAILED');
|
||||
expect(result.stdout).not.toContain('FF_REFUSED');
|
||||
expect(result.stdout).not.toContain('FF_OK');
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -65,11 +65,37 @@ describe('workflow judge excerpts', () => {
|
||||
expect(text).toContain('## Step 9:');
|
||||
});
|
||||
|
||||
test('plan review evidence and design approval rules precede their use', () => {
|
||||
const eng = readWorkflowExcerpt('plan-eng-review/SKILL.md', '## Review Sections', '## CRITICAL RULE');
|
||||
expect(eng.indexOf('## Confidence Calibration')).toBeLessThan(eng.indexOf('### 1. Architecture review'));
|
||||
expect(eng).toContain('quote the motivating plan requirement');
|
||||
expect(eng).toContain('including all Claude fallback modes');
|
||||
expect(eng).toContain('no in-host substitute is defined here');
|
||||
const design = readWorkflowExcerpt('plan-design-review/SKILL.md', '## Review Sections', '## CRITICAL RULE');
|
||||
expect(design).toContain('wait for approval, then edit the plan and re-rate');
|
||||
const pass4 = design.slice(design.indexOf('### Pass 4:'), design.indexOf('### Pass 5:'));
|
||||
expect(pass4.indexOf('### Design Hard Rules')).toBeLessThan(pass4.indexOf('FIX TO 10:'));
|
||||
expect(pass4).toContain('caps this pass below 8');
|
||||
});
|
||||
|
||||
test('fails closed for missing excerpt markers', () => {
|
||||
expect(() => readWorkflowExcerpt('ship/SKILL.md', '# missing', null)).toThrow('Start marker not found');
|
||||
expect(() => readWorkflowExcerpt('ship/SKILL.md', '# Ship:', '# missing')).toThrow('End marker not found');
|
||||
});
|
||||
|
||||
test('deploy gates and navigation timing formulas are executable as documented', () => {
|
||||
const land = readFileSync(join(import.meta.dir, '../land-and-deploy/SKILL.md.tmpl'), 'utf8');
|
||||
expect(land).not.toContain('Skip Step 3, go to Step 4');
|
||||
expect(land).toContain('continue to Step 3.4, then Step 3.5 before merging');
|
||||
const benchmark = readFileSync(join(import.meta.dir, '../benchmark/SKILL.md.tmpl'), 'utf8');
|
||||
const timings = { startTime: 0, domInteractive: 600, domComplete: 1200, loadEventEnd: 1400 };
|
||||
for (const [label, expected] of [['DOM Interactive', 600], ['DOM Complete', 1200], ['Full Load', 1400]] as const) {
|
||||
const formula = benchmark.match(new RegExp(`\\*\\*${label}\\*\\*: \x60([^\x60]+)\x60`))![1];
|
||||
const actual = new Function(...Object.keys(timings), `return ${formula}`)(...Object.values(timings));
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('WIP squash example consumes the prepared todo and preserves file contents', () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'ship-wip-example-'));
|
||||
const env = {
|
||||
|
||||
Reference in New Issue
Block a user