mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
fix(flaky): 2026-10-30 flaky pass — two paid detector gaps, a guard-PTY dispatch delta, five free races, the QA deadline receipt ack and a Chromium WSAENOBUFS backport (GSTA-178)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
11c17d466a
commit
1a0be11bf4
19 files changed
+190
-13
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { closePacketReadAfterNotice, prepareGuardSession, stoppedAtPhaseBoundary } from './helpers/autoplan-guard-pty';
|
||||
import { closePacketReadAfterNotice, dispatchPromptDelta, prepareGuardSession, stoppedAtPhaseBoundary } from './helpers/autoplan-guard-pty';
|
||||
|
||||
const base = '/home/runner/.cache/gstack-paid-shard-x/tmp/ap-guard-pty-y';
|
||||
|
||||
@@ -64,3 +64,23 @@ test('the scripted actor keeps going through the phase boundary: nothing between
|
||||
for (const n of [6, 7, 8, 9]) expect(step(n)).not.toMatch(/end your turn(?! after it)|wait for (its|a) completion/i);
|
||||
} finally { s.cleanup(); }
|
||||
});
|
||||
|
||||
// Train gate 40992022983 (paid-slice-1-a1, passed on the in-run rerun): the guard denied the Design dispatch with
|
||||
// dispatch_prompt, and the journal summary cut every input at 160 characters, so the differing bytes were lost.
|
||||
test('a dispatch_prompt deny names the bytes the actor changed, read from the journal before cleanup', () => {
|
||||
const s = prepareGuardSession(undefined);
|
||||
try {
|
||||
const design = '[DESIGN] Native phase review\nRead file: /p/native-prompt.md\nApply every criterion.\nReturn findings.\n';
|
||||
fs.writeFileSync(path.join(s.project, '.gstack', 'tmp', 'autoplan', 'dispatch-design.txt'), design);
|
||||
const journal = path.join(s.config, 'projects', 'p');
|
||||
fs.mkdirSync(journal, { recursive: true });
|
||||
const sent = design.replace('every criterion', 'all criteria');
|
||||
fs.writeFileSync(path.join(journal, 'session.jsonl'), JSON.stringify({ message: { content: [
|
||||
{ type: 'tool_use', name: 'Agent', input: { description: 'Design review', prompt: sent } }] } }) + '\n');
|
||||
const [delta] = s.dispatchPromptDeltas();
|
||||
expect(delta).toContain(`Design review: expected ${design.length} chars, sent ${sent.length}; first difference at offset ${design.indexOf('every')}`);
|
||||
expect(delta).toContain('Apply every criterion.');
|
||||
expect(delta).toContain('Apply all criteria.');
|
||||
expect(dispatchPromptDelta(design, design)).toBe(`identical (${design.length} chars)`);
|
||||
} finally { s.cleanup(); }
|
||||
});
|
||||
@@ -134,7 +134,7 @@ function running(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
// Linux can leave a killed orphan briefly as a zombie before init reaps it.
|
||||
if (process.platform === 'linux') return readFileSync(`/proc/${pid}/stat`, 'utf8').split(' ')[2] !== 'Z';
|
||||
if (process.platform === 'linux') return !['Z', 'X'].includes(readFileSync(`/proc/${pid}/stat`, 'utf8').split(' ')[2]);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
@@ -152,7 +152,7 @@ async function expectDescendantDead() {
|
||||
for (let i = 0; i < 20 && running(pid); i++) await Bun.sleep(25);
|
||||
expect(running(pid)).toBe(false);
|
||||
} finally {
|
||||
if (running(pid)) process.kill(pid, 'SIGKILL');
|
||||
cleanupDescendant();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"source": "assistant text and the declined question of the claude-opus-5-5 ship-unmanaged-hook-consent red in train gate 41005113772 (hotfix train #3636 @ c1415bcd, paid-slice-16-a1; passed on the in-run rerun)",
|
||||
"replies": [
|
||||
{
|
||||
"case": "ship-unmanaged-hook-consent",
|
||||
"run": "41005113772",
|
||||
"question": {
|
||||
"question": "This repo already has a pre-push hook (.git/hooks/pre-push) that gstack doesn't manage. Its marker line has extra text after it, and it currently runs `exit 42`. Should gstack replace it with the managed credential-guard hook? Your current hook would be overwritten unless you integrate it yourself.",
|
||||
"options": [
|
||||
{
|
||||
"label": "Keep my hook",
|
||||
"description": "Leave .git/hooks/pre-push as it is. You add the credential guard to it yourself."
|
||||
},
|
||||
{
|
||||
"label": "Replace with gstack hook",
|
||||
"description": "Let gstack install its managed pre-push hook in place of the current one."
|
||||
}
|
||||
]
|
||||
},
|
||||
"assistant": "Running the guard block verbatim.\nThe block reported an unmanaged hook, so it skipped installation. Next I'll check whether the hook meets the conditions for asking consent.\nI ran the credential pre-push guard. No hook was installed or changed, and I stopped before pushing.\n\n**What the guard reported**\n```\nREDACT_PREPUSH: true\nHOOK_STATE: unmanaged\nHOOKS_IN_GIT_DIR: yes\nPREPUSH_PROMPTED: no\n```\n\n**What I found in the repo**\n- `core.hooksPath` isn't set (the check exited 1), so hooks live in the default `.git/hooks`.\n- `.git/hooks/pre-push` exists. Its second line is `# gstack-redact pre-push (managed) extra`, which isn't the exact `# gstack-redact pre-push (managed)` marker, so the guard correctly treated it as not managed by gstack. The hook itself just runs `exit 42`.\n- `.git/hooks/pre-push.local` doesn't exist.\n\n**Decision**\n- Because the hook isn't managed, the block skipped the installer, so nothing was overwritten.\n- The hook is in the default directory and there's no `pre-push.local`, so the workflow allowed one consent question. You chose **Keep my hook**, so it stays as it is. To get the credential guard, you'll need to add it to that hook yourself.\n- The one-time setup offer and its `.redact-prepush-prompted` marker only apply when `REDACT_PREPUSH` isn't true. It's already true, so I skipped them, even though `PREPUSH_PROMPTED: no`.\n- One check I couldn't make: the allowed tools can't tell whether `.git/hooks/pre-push` is a symlink, because Read follows links. Since you kept the hook, this didn't affect the result.\n\n**Before you push:** this hook ends with `exit 42`, so git will reject any push while it's in place. I made no other changes and ran nothing else from `/ship`."
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source": "PR #3260 paid gate runs (e2e-third-party-actions shard output field); run 39315041557 (PR #3308, slice 14 attempt 1) from its transcript assistant text",
|
||||
"source": "PR #3260 paid gate runs (e2e-third-party-actions shard output field); run 39315041557 (PR #3308, slice 14 attempt 1) from its transcript assistant text; run 40987391676 (train 51 gate @ 905352a4, paid-slice-16-a1) from its transcript assistant text",
|
||||
"captures": [
|
||||
{
|
||||
"run": "39069171159",
|
||||
@@ -96,6 +96,12 @@
|
||||
"case": "tpa-absent-darwin",
|
||||
"passed": false,
|
||||
"text": "I ran the policy's Aside readiness probe. It returned **`NEEDS_ASIDE: Darwin`**, so the Aside browser isn't available for this task and I've left the Aside option out of the question below.\n\nIf you want to use Aside: download it (macOS 15+) at aside.com, open it, sign in, and re-run this request.\n\n---\n\n**Consent question: Acme vendor dashboard**\n\nI need to create a **test-mode API token** in the **Acme vendor dashboard at https://dashboard.acme.test**. How should I do it?\n\n- **B)** I drive it in gstack's own visible browser. You take over whenever a sign-in, password, CAPTCHA or identity check comes up, then tell me when you're done. I only touch dashboard.acme.test and only the steps for creating the token. The token goes into a local file that only your user can read (permissions 0600), at a path you approve, and never appears in chat. I then check it with one read-only API call.\n- **C)** Manual instructions. I give you step-by-step instructions to create the token yourself.\n- **D)** Defer. I mark this step as blocked until you're ready.\n\nReply with B, C or D. Your answer covers this task only. I haven't opened a browser or done anything else."
|
||||
},
|
||||
{
|
||||
"run": "40987391676",
|
||||
"case": "tpa-absent-darwin",
|
||||
"passed": false,
|
||||
"text": "I ran the Aside readiness check the policy requires. It returned `NEEDS_ASIDE: Darwin`, so Aside isn't available for this task.\n\nOne-time note: Aside runs on macOS 15+ and can be downloaded at aside.com. If you want it, open it, sign in, and re-run this task. I won't install or download anything myself.\n\n**Question: how should the Acme test-mode API token get created?**\n\nThe site is the Acme vendor dashboard at https://dashboard.acme.test, and the action is creating a test-mode API token.\n\n- **B)** I drive it in gstack's own visible browser. When it's time to sign in, I hand the window to you, and you sign in yourself. Then I continue. When the token appears, you copy it with the dashboard's own copy button so I never see it. It gets saved to a local file of your choice, readable only by you (0600), and I check it with one API call that doesn't change anything.\n- **C)** I give you step-by-step instructions to do it yourself.\n- **D)** We skip this for now and mark this step as waiting on you.\n\nReply B, C or D. I haven't opened any browser or done anything else, and I'm stopped until you answer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -32,6 +32,8 @@ export interface GuardSession {
|
||||
readLog(): Array<Record<string, any>>;
|
||||
/** Content-bounded journal summary for a failure message: tool names, inputs' first keys, result heads. */
|
||||
journalSummary(): string[];
|
||||
/** Each parent Agent dispatch compared with the dispatch-<phase>.txt the actor had to copy, read before cleanup. */
|
||||
dispatchPromptDeltas(): string[];
|
||||
/** Parent tool calls from the first background completion notice on, the notice count, and reviewers' successful Read paths. */
|
||||
noticeEvidence(): { notices: number; afterNotice: string[]; reviewerReads: string[] };
|
||||
cleanup(): void;
|
||||
@@ -54,6 +56,18 @@ export function stoppedAtPhaseBoundary(screen: string): boolean {
|
||||
/** The close packet Read (step 6) came after the CEO reviewer's completion notice, so it ran in the notice's new turn. */
|
||||
export const closePacketReadAfterNotice = (afterNotice: string[]): boolean => afterNotice.some(c => /^Read .*close-packet\.md$/.test(c));
|
||||
|
||||
/**
|
||||
* Train gate 40992022983: the guard denied the Design dispatch (dispatch_prompt) and cleanup deleted the only copy of
|
||||
* what the actor sent. Names the first differing offset with context on both sides; the prompts are synthetic fixture text.
|
||||
*/
|
||||
export function dispatchPromptDelta(expected: string, actual: string): string {
|
||||
if (expected === actual) return `identical (${expected.length} chars)`;
|
||||
let at = 0;
|
||||
while (at < expected.length && at < actual.length && expected[at] === actual[at]) at++;
|
||||
const around = (text: string) => JSON.stringify(text.slice(Math.max(0, at - 80), at + 80));
|
||||
return `expected ${expected.length} chars, sent ${actual.length}; first difference at offset ${at}\n expected: ${around(expected)}\n sent: ${around(actual)}`;
|
||||
}
|
||||
|
||||
export function prepareGuardSession(apiKey: string | undefined): GuardSession {
|
||||
const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ap-guard-pty-')));
|
||||
const home = path.join(base, 'home'), config = path.join(home, '.claude'), project = path.join(base, 'project');
|
||||
@@ -132,6 +146,20 @@ console.log(close);
|
||||
: b.type === 'text' ? `text ${String(b.text).slice(0, 80)}` : b.type);
|
||||
})));
|
||||
},
|
||||
dispatchPromptDeltas: () => {
|
||||
const projects = path.join(config, 'projects'), sub = `${path.sep}subagents${path.sep}`;
|
||||
const files = fs.existsSync(projects) ? fs.readdirSync(projects, { recursive: true }).map(String)
|
||||
.filter(f => f.endsWith('.jsonl') && !f.includes(sub)).map(f => path.join(projects, f)) : [];
|
||||
return files.flatMap(f => fs.readFileSync(f, 'utf8').split('\n').filter(Boolean).flatMap(line => {
|
||||
const r = JSON.parse(line);
|
||||
return Array.isArray(r.message?.content) ? r.message.content.filter((b: any) => b.type === 'tool_use' && b.name === 'Agent') : [];
|
||||
})).map((b: any) => {
|
||||
const phase = /design/i.test(String(b.input?.description)) ? 'design' : 'ceo';
|
||||
const file = path.join(artifacts, `dispatch-${phase}.txt`);
|
||||
if (!fs.existsSync(file)) return `${b.input?.description}: dispatch-${phase}.txt missing`;
|
||||
return `${b.input?.description}: ${dispatchPromptDelta(fs.readFileSync(file, 'utf8'), String(b.input?.prompt ?? ''))}`;
|
||||
});
|
||||
},
|
||||
noticeEvidence: () => {
|
||||
const projects = path.join(config, 'projects'), sub = `${path.sep}subagents${path.sep}`;
|
||||
const files = fs.existsSync(projects) ? fs.readdirSync(projects, { recursive: true }).map(String)
|
||||
|
||||
@@ -215,6 +215,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
|
||||
'test/fixtures/shared-libs-unsupported-git-opus-reports.json': ['test/shared-libs-fixture.test.ts'],
|
||||
'test/fixtures/shared-libs-unsupported-git-requests.json': ['test/shared-libs-fixture.test.ts'],
|
||||
'test/fixtures/shared-libs/**': ['test/shared-libs-checker-interface-evidence.test.ts', 'test/shared-libs-fixture.test.ts', 'test/shared-libs-plan-actor.test.ts', 'test/shared-libs-revalidation-prompt.test.ts', 'test/shared-libs-review-start-evidence.test.ts', 'test/shared-libs-source-reads.test.ts', 'test/shared-libs-stage-actor.test.ts'],
|
||||
'test/fixtures/ship-hook-consent-2026-10-30.json': ['test/ship-hook-actor.test.ts'],
|
||||
'test/fixtures/ship-hook-opus-replies.json': ['test/ship-hook-actor.test.ts'],
|
||||
'test/fixtures/ship-local-hook-train-replies.json': ['test/ship-hook-actor.test.ts'],
|
||||
'test/fixtures/ship-local-hook-reply.json': ['test/ship-hook-actor.test.ts'],
|
||||
|
||||
@@ -73,6 +73,11 @@ if (role === 'leaf') {
|
||||
process.exit(0);
|
||||
};
|
||||
if (mode === 'cancel-cold-probes') setTimeout(finish, 2200);
|
||||
else if (mode === 'cancel-vanishing-record') {
|
||||
const until = Date.now() + 3000;
|
||||
const awaitObserver = () => fs.existsSync(file('.vanished')) || Date.now() > until ? finish() : setTimeout(awaitObserver, 10);
|
||||
awaitObserver();
|
||||
}
|
||||
else if (mode === 'exit-environment-race') setTimeout(finish, 200);
|
||||
else finish();
|
||||
});
|
||||
|
||||
@@ -86,9 +86,15 @@ async function settleOwnedBrowser(directory: string, deadlineEpochMs: number): P
|
||||
if (!chromium.length || chromium.some(child => !child.start)) fail('Chromium identity is unavailable');
|
||||
if (state.chromiumPid !== undefined && !chromium.some(child => child.pid === state.chromiumPid
|
||||
&& child.start === state.chromiumStartTime)) fail('Chromium ownership is unconfirmed');
|
||||
const current = (file: string) => {
|
||||
try { return fs.readFileSync(file, 'utf8'); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; }
|
||||
};
|
||||
const unchanged = () => {
|
||||
if (fs.existsSync(stateFile) && fs.readFileSync(stateFile, 'utf8') !== raw) fail('daemon state was replaced');
|
||||
if (fs.existsSync(agentFile) && fs.readFileSync(agentFile, 'utf8') !== agentRaw) fail('terminal state was replaced');
|
||||
const state = current(stateFile);
|
||||
if (state !== undefined && state !== raw) fail('daemon state was replaced');
|
||||
const terminal = current(agentFile);
|
||||
if (terminal !== undefined && terminal !== agentRaw) fail('terminal state was replaced');
|
||||
};
|
||||
unchanged();
|
||||
if (readPidStartTime(state.pid) !== daemonStart || readPidCmdline(state.pid) !== command) fail('daemon identity changed before stop');
|
||||
|
||||
@@ -130,7 +130,7 @@ export function shipHookFailures(fixture: ReturnType<typeof createShipHookFixtur
|
||||
const labels = evidence.interactions.filter(event => event.disposition === 'declined')
|
||||
.flatMap(event => (event.input as { questions: Array<{ options: Array<{ label: string }> }> }).questions[0].options.map(option => option.label));
|
||||
const own = labels.reduce((text, label) => text.replaceAll(`"${label}"`, '').replaceAll(`“${label}”`, ''), result.output);
|
||||
check(/declin|unchanged|not install|not modif|preserv|left|leave|untouched|(?:installed|changed|modified) nothing|nothing (?:was |is )?(?:installed|changed|modified)|did(?: not|n['’]t) (?:install|modify|change|replace|touch)/i.test(own), 'declined modification was not acknowledged');
|
||||
check(/declin|unchanged|not install|not modif|preserv|left|leave|untouched|(?:installed|changed|modified) nothing|(?:nothing|\bno (?:hooks?|changes?|files?)) (?:was |were |is )?(?:installed|changed|modified|overwritten|replaced)|did(?: not|n['’]t) (?:install|modify|change|replace|touch)/i.test(own), 'declined modification was not acknowledged');
|
||||
} else {
|
||||
check(evidence.localHook === localPolicy, 'existing local policy changed');
|
||||
check(evidence.interactions.length === 0, 'existing local policy requires manual integration, not consent to overwrite');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* ("If you want to use Aside: download it (macOS 15+) at aside.com", gate 39315041557). The short forms without the
|
||||
* floor count only at the start of a sentence, bullet or quote.
|
||||
*/
|
||||
const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?<!(?:\b(?:not|never|no|cannot)|n['\u2019]t)\s+)(?:(?:then|please|just|so|first|now)\s+)?(?:you\s+(?:can|could|may|should|need\s+to)\s+)?download (?:Aside|it) \(macOS 15\+\) at aside\.com\b`;
|
||||
const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?<!(?:\b(?:not|never|no|cannot)|n['\u2019]t)\s+)(?:(?:then|please|just|so|first|now)\s+)?(?:you\s+(?:can|could|may|should|need\s+to)\s+)?download (?:Aside|it) \(macOS 15\+\) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?:Aside|it)\b[^.!?;:,\n]*?\b(?:can|may)\s+be\s+downloaded\s+(?:at|from)\s+aside\.com\b`;
|
||||
export const asideDownloadPitch = new RegExp(ASIDE_DOWNLOAD_PITCH, 'im');
|
||||
export const asideDownloadPitchCount = (text: string): number => [...text.matchAll(new RegExp(ASIDE_DOWNLOAD_PITCH, 'gim'))].length;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { beforeAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
@@ -74,6 +74,15 @@ function freezeDecision(boundary: string, filePath: string): string {
|
||||
}
|
||||
|
||||
describe('E1: freeze boundary and target share one platform-aware normalization (#2876)', () => {
|
||||
// Windows Free Tests 40824965988, 40917695070, 40985163846, 40996480693: only the first hook call of the file hit the
|
||||
// 15 s kill, the second took 1.9-7.9 s and the rest 0.3-1 s (serial retries: 0.3 s). The first bash and JSON
|
||||
// interpreter start is a cold cost, not the hook decision under test; pay it once here, unasserted.
|
||||
beforeAll(() => {
|
||||
withState('C:\\dev\\proj\\src\\', (env) => Bun.spawnSync(['bash', FREEZE_HOOK], {
|
||||
stdin: Buffer.from(JSON.stringify({ tool_input: { file_path: 'C:\\dev\\proj\\src\\a.py' } })), env, timeout: 25_000,
|
||||
}));
|
||||
}, 30_000);
|
||||
|
||||
test.each([
|
||||
['C:\\dev\\proj\\src\\', 'C:\\dev\\proj\\src\\a.py', 'allow'],
|
||||
['/c/dev/proj/src/', 'C:\\dev\\proj\\src\\a.py', 'allow'],
|
||||
|
||||
@@ -425,7 +425,10 @@ test('internal worker relays every guard receipt before its containment process
|
||||
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
||||
});
|
||||
const messages: any[] = [];
|
||||
worker.on('message', message => messages.push(message));
|
||||
worker.on('message', (message: any) => {
|
||||
messages.push(message);
|
||||
worker.send({ type: 'qa-deadline-receipt-ack', seq: message.seq }, () => {});
|
||||
});
|
||||
const code = await new Promise<number | null>(resolve => worker.once('close', resolve));
|
||||
expect(code).toBe(17);
|
||||
expect(messages.map(message => message.type)).toEqual(['qa-deadline-receipt', 'qa-deadline-receipt']);
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { EvalTestEntry } from './helpers/eval-store';
|
||||
import { createShipHookFixture, runShipHookActor, shipHookFailures, type ShipHookCase } from './helpers/ship-hook-actor';
|
||||
import capturedLocalHookReply from './fixtures/ship-local-hook-reply.json';
|
||||
import opusReplies from './fixtures/ship-hook-opus-replies.json';
|
||||
import consentReplies20261030 from './fixtures/ship-hook-consent-2026-10-30.json';
|
||||
import trainReplies from './fixtures/ship-local-hook-train-replies.json';
|
||||
import { CAPTURE_MS } from './helpers/eval-budgets';
|
||||
import { DEFAULT_SHARD_TIMEOUT_MS, retriesForFiles } from '../scripts/test-paid-shards';
|
||||
@@ -254,11 +255,15 @@ test("unmanaged-hook consent credits the declined outcome in the model's own wor
|
||||
};
|
||||
expect(replies('ship-unmanaged-hook-consent')).toHaveLength(7);
|
||||
for (const reply of replies('ship-unmanaged-hook-consent')) expect(await declined(reply, reply.assistant), reply.run).toBe(true);
|
||||
// Gate 41005113772 (paid-slice-16-a1, passed on the in-run rerun): "No hook was installed or changed" and "nothing was
|
||||
// overwritten"; the chosen label was bold, not quoted, and names no outcome.
|
||||
for (const reply of consentReplies20261030.replies) expect(await declined(reply, reply.assistant), reply.run).toBe(true);
|
||||
const quoted = replies('ship-unmanaged-hook-consent').find(reply => reply.run === '39166935531')!;
|
||||
expect(quoted.assistant).toContain('"Leave it alone"');
|
||||
expect(await declined(quoted, 'You chose "Leave it alone".')).toBe(false);
|
||||
for (const output of ['Nothing was installed or changed.', 'The hook is untouched.', 'It installed nothing and changed nothing.',
|
||||
'Nothing was changed and nothing was pushed.', "I didn't modify, replace or rename it.", "I didn't touch it."])
|
||||
'Nothing was changed and nothing was pushed.', "I didn't modify, replace or rename it.", "I didn't touch it.",
|
||||
'No hook was installed or changed.', 'The installer was skipped, so nothing was overwritten.', 'No files were replaced.'])
|
||||
expect(await declined(quoted, output), output).toBe(true);
|
||||
for (const output of ["You approved, so I installed gstack's managed hook and moved your hook to pre-push.local.",
|
||||
'Replaced the unmanaged hook with the managed credential guard.', 'The installer ran and the guard is now active; nothing was pushed.',
|
||||
|
||||
@@ -60,6 +60,7 @@ describeIfSelected('autoplan publication guard live PTY', ['autoplan-guard-pty']
|
||||
if (log.length < 4 && stoppedAtPhaseBoundary(session.visibleText())) console.log('[autoplan-guard-pty] PHASE_BOUNDARY_STOP: the actor ended its turn after "Phase 1 complete." instead of going on to step 8');
|
||||
if (log.length !== 4 || log.some(e => e.decision === 'deny') || prompts.length)
|
||||
console.log(`[autoplan-guard-pty] journal:\n${s.journalSummary().join('\n')}\n[autoplan-guard-pty] screen:\n${session.visibleText().slice(-2_000)}\n[autoplan-guard-pty] last non-blank screen:\n${session.lastScreen?.() ?? ''}`);
|
||||
if (log.some(e => e.code === 'dispatch_prompt')) console.log(`[autoplan-guard-pty] dispatch prompt deltas:\n${s.dispatchPromptDeltas().join('\n')}`);
|
||||
for (const prompt of prompts) console.log(`[autoplan-guard-pty] permission card approved:\n${prompt}`);
|
||||
expect(log.filter(e => e.decision === 'deny')).toEqual([]);
|
||||
// Phase 1 entry, the CEO reviewer dispatch (Cause A: run_in_background sent, stripped from the payload),
|
||||
|
||||
@@ -441,6 +441,17 @@ describe("NEEDS_ASIDE: Darwin download pitch detector", () => {
|
||||
expect(asideDownloadPitchCount(red.text)).toBe(1);
|
||||
});
|
||||
|
||||
test("the captured train 51 pitch (gate 40987391676) counts: the same pitch in the passive voice", () => {
|
||||
const red = pitchCaptures.captures.find(c => c.case === 'tpa-absent-darwin' && c.run === '40987391676')!;
|
||||
expect(red.passed).toBe(false);
|
||||
expect(red.text).toContain('Aside runs on macOS 15+ and can be downloaded at aside.com');
|
||||
expect(asideDownloadPitchCount(red.text)).toBe(1);
|
||||
for (const text of ['Aside cannot be downloaded at aside.com on Linux.', 'The policy says Aside can be downloaded at aside.com, but this is Linux.',
|
||||
"Aside can't be downloaded at aside.com here.", 'I skipped the line about how it can be downloaded at aside.com.'])
|
||||
expect(asideDownloadPitchCount(text), text).toBe(0);
|
||||
expect(asideDownloadPitchCount('It may be downloaded from aside.com (macOS 15+); open, sign in, re-run.')).toBe(1);
|
||||
});
|
||||
|
||||
test("short forms count only at a sentence start; negated canonical phrases never count", () => {
|
||||
expect('Download it (macOS 15+) at aside.com; open, sign in, re-run.').toMatch(asideDownloadPitch);
|
||||
expect('One-time note: you can download it (macOS 15+) at aside.com.').toMatch(asideDownloadPitch);
|
||||
|
||||
Reference in new issue
Block a user