mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-15 04:47:23 +01:00
fix(hooks): option values are not the check verb, wrapper chains stay linear, agent stalls are timed, report re-checks deferred runs (GST-1088 follow-up review)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
365dd613d2
commit
7595189caf
6 files changed
+98
-39
No files matched your search
@@ -7,9 +7,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { STATE_ERROR_DOC, stateErrorLine } from '../lib/skill-contract';
|
||||
import { AGENT_STALL_MS, STATE_ERROR_DOC, stateErrorLine } from '../lib/skill-contract';
|
||||
import {
|
||||
CONTRACT_HOOK, DEBUG_REPORT, INTERACTIVE_TURNS, J1_TRANSCRIPT, ROOT, hookEnv, result, runHook, say, skillsDir, slash, tmpDir, tool, user,
|
||||
CONTRACT_HOOK, DEBUG_REPORT, INTERACTIVE_TURNS, J1_TRANSCRIPT, ROOT, hookEnv, meta, result, runHook, say, skillsDir, slash, tmpDir, tool, user,
|
||||
} from './helpers/skill-contract-fixture';
|
||||
|
||||
const PINNED = fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'skill-contract', 'block-message.txt'), 'utf8').trimEnd();
|
||||
@@ -135,7 +135,7 @@ describe('never blocks the wrong turn', () => {
|
||||
expect(JSON.parse(stop(file, { GSTACK_HEADLESS: '1' }).stdout).decision).toBe('block');
|
||||
});
|
||||
|
||||
test('Agent readers still running when Stop fires: allowed once and recorded deferred; the same set still running next stop is checked', () => {
|
||||
test('Agent readers still running when Stop fires: allowed and recorded deferred until the same set has run AGENT_STALL_MS, then checked', () => {
|
||||
const base = [slash('review'), say('Dispatching 2 specialists: Testing, Security. Skipped: Performance (scope not detected).')];
|
||||
const background = [...base, tool('a1', 'Agent', { description: 'Testing specialist', prompt: 'review', run_in_background: true }),
|
||||
result('a1', 'Async agent launched in the background.'), say('Waiting for the specialists to finish.')];
|
||||
@@ -143,7 +143,13 @@ describe('never blocks the wrong turn', () => {
|
||||
expect(stop(file)).toEqual({ code: 0, stdout: '', stderr: '' });
|
||||
expect(Object.values(record().runs)[0]).toMatchObject({ outcome: 'deferred', deferred_agents: ['a1'] });
|
||||
expect(fs.readFileSync(path.join(state, 'hooks', 'skill-contract.log'), 'utf8')).toContain('event=agents-running');
|
||||
fs.appendFileSync(file, `${say('Still waiting on the specialists.')}\n`);
|
||||
fs.appendFileSync(file, `${user('how is it going?')}\n${meta('<task-notification>bash b9 completed</task-notification>')}\n${say('Still waiting on the specialists.')}\n`);
|
||||
expect(stop(file)).toEqual({ code: 0, stdout: '', stderr: '' });
|
||||
expect(Object.values(record().runs)[0]).toMatchObject({ outcome: 'deferred', deferred_agents: ['a1'] });
|
||||
const rec = record();
|
||||
const key = Object.keys(rec.runs)[0];
|
||||
rec.runs[key].deferred_at = new Date(Date.now() - AGENT_STALL_MS - 1000).toISOString();
|
||||
fs.writeFileSync(path.join(state, 'skill-contracts', 'sess-1.json'), JSON.stringify(rec));
|
||||
expect(JSON.parse(stop(file).stdout).decision).toBe('block');
|
||||
expect(Object.values(record().runs)[0]).toMatchObject({ outcome: 'blocked' });
|
||||
expect(fs.readFileSync(path.join(state, 'hooks', 'skill-contract.log'), 'utf8')).toContain('event=agents-stalled');
|
||||
@@ -151,6 +157,13 @@ describe('never blocks the wrong turn', () => {
|
||||
expect(stop(transcript(unanswered, 'unanswered.jsonl'), {}, { session_id: 'sess-2' })).toEqual({ code: 0, stdout: '', stderr: '' });
|
||||
});
|
||||
|
||||
test('a stop on a turn that ends on a question never starts the agent clock', () => {
|
||||
const asking = [slash('review'), tool('a1', 'Agent', { description: 'Testing specialist', prompt: 'review', run_in_background: true }),
|
||||
result('a1', 'Async agent launched in the background.'), say('Should I also review the migrations?')];
|
||||
expect(stop(transcript(asking))).toEqual({ code: 0, stdout: '', stderr: '' });
|
||||
expect(fs.existsSync(path.join(state, 'skill-contracts', 'sess-1.json'))).toBe(false);
|
||||
});
|
||||
|
||||
test('no contract skill loaded: allowed', () => {
|
||||
const r = stop(transcript([slash('qa'), say('Done. STATUS: DONE')]));
|
||||
expect(r).toEqual({ code: 0, stdout: '', stderr: '' });
|
||||
|
||||
@@ -8,7 +8,8 @@ import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
DEFAULT_MATCHERS, EXIT, STATUS_RE, blockMessage, checkContract, isCheckCommand, isPlaceholder, parseContract, parseTranscript, skillLoads, UnknownShape,
|
||||
DEFAULT_MATCHERS, EXIT, STATUS_RE, blockMessage, checkContract, isCheckCommand, isMiss, isPlaceholder, parseContract, parseTranscript, reportRows, skillLoads, UnknownShape,
|
||||
type CheckResult,
|
||||
type Contract,
|
||||
} from '../lib/skill-contract';
|
||||
import { readFrontmatter } from '../lib/skill-route';
|
||||
@@ -183,6 +184,26 @@ describe('checkContract is a pure function of the transcript', () => {
|
||||
expect([cmd, isCheckCommand(cmd)]).toEqual([cmd, false]);
|
||||
});
|
||||
|
||||
test('check_after_last_edit: an option value is never the verb, and a long chain of wrapper words stays fast', () => {
|
||||
for (const cmd of ['make -C web test', 'npm --prefix web test', 'pnpm --filter app test', 'bun --cwd app test', 'yarn workspace app test',
|
||||
'xcodebuild -scheme App test', 'cargo +nightly test', 'mvn clean test', 'bun run typecheck', 'node --test'])
|
||||
expect([cmd, isCheckCommand(cmd)]).toEqual([cmd, true]);
|
||||
for (const cmd of ['make -C web build', 'npm --prefix web install', 'pnpm --filter app build', 'xcodebuild -scheme App build'])
|
||||
expect([cmd, isCheckCommand(cmd)]).toEqual([cmd, false]);
|
||||
const started = performance.now();
|
||||
expect(isCheckCommand(`${Array(40).fill('sh').join(' ')} echo`)).toBe(false);
|
||||
expect(performance.now() - started).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('report re-checks a deferred run: unmet is a miss, met is met', () => {
|
||||
const run = { skill: 'review', load_line: 1, outcome: 'deferred' as const, missing: [], waived: [], deferred_agents: ['a1'], updated_at: '' };
|
||||
const rec = { version: 1 as const, session_id: 's', transcript_path: 't', runs: { k: run } };
|
||||
const unmet = reportRows([rec], () => ({ met: false, missing: ['specialists'], waived: [] }) as unknown as CheckResult);
|
||||
expect(unmet.map(r => [r.outcome, isMiss(r)])).toEqual([['miss', true]]);
|
||||
const met = reportRows([rec], () => ({ met: true, missing: [], waived: [] }) as unknown as CheckResult);
|
||||
expect(met.map(r => [r.outcome, isMiss(r)])).toEqual([['met', false]]);
|
||||
});
|
||||
|
||||
test('selected_specialists_dispatched: a selection line only counts from assistant text or a Bash result, with plain names', () => {
|
||||
const red = [tool('c', 'Agent', { description: 'Red team', prompt: 'Think like an attacker and a chaos engineer.' }), result('c', '[]')];
|
||||
const report = say('Pre-Landing Review: 0 blocking\nGate edits: none\nSTATUS: DONE');
|
||||
|
||||
Reference in new issue
Block a user