fix(harness): read QM's recorded import error for the cap; refuse to size a non-git checkout; skip symlinks like QM

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-28 07:24:00 +00:00
1 parent b8bbf0f3bc
commit 9e258e21bb
2 files changed
+25 -10

No files matched your search

+9 -6
View File
@@ -30,11 +30,12 @@ const tooLarge = (what: string) => new HarnessError('HARNESS_QM_PACK_TOO_LARGE',
/** Files and bytes QM would read from the repository at ref. */
export function packSize(checkout: string, ref: string): { files: number; bytes: number } {
const r = spawnSync('git', ['-C', checkout, 'ls-tree', '-r', '-l', ref], { encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024 });
if (r.status !== 0 || !ref) throw new HarnessError('HARNESS_QM_API', 'could not read the gstack checkout at ' + (ref || 'HEAD') + ' to size the pack (QM imports a git commit, so register qm needs a git checkout of gstack)');
let files = 0; let bytes = 0;
for (const line of (r.stdout ?? '').split('\n')) {
const size = Number(line.split(/\s+/)[3]);
if (line.split(/\s+/)[1] !== 'blob' || !Number.isFinite(size)) continue;
files++; bytes += size;
const [mode, type, , size] = line.split(/\s+/);
if (type !== 'blob' || mode === '120000' || !Number.isFinite(Number(size))) continue;
files++; bytes += Number(size);
}
return { files, bytes };
}
@@ -71,8 +72,6 @@ async function call(auth: QmAuth, method: string, pathname: string, payload?: un
headers: { 'content-type': 'application/json', ...auth.headers(method, pathname, body) },
});
const text = await res.text();
const cap = /pack exceeds max (bytes|files) \(\d+\)/.exec(text);
if (!res.ok && cap) throw tooLarge('QM refused the pack: ' + cap[0]);
if (!res.ok) throw new HarnessError('HARNESS_QM_API', method + ' ' + pathname + ' returned HTTP ' + res.status + ': ' + text.slice(0, 200));
return text ? JSON.parse(text) : {};
} catch (e) {
@@ -111,7 +110,11 @@ export async function registerQm(opts: { checkout: string; stateRoot: string; en
const pack = existing ?? (await call(auth, 'POST', '/v1/admin/skill-packs', { url: GSTACK_REPO_URL, ref, config: QM_PACK_CONFIG, trustTier: 'third-party' })).pack;
if (!pack?.id) throw new HarnessError('HARNESS_QM_API', 'QM did not return a pack id');
writeLedger(lfile, { meta: { id, target: 'qm', dest: auth.url, mode: 'all', checkout: opts.checkout, version: opts.version, qm_pack: pack.id, qm_ref: ref }, files: [], dirs: [], blocks: [] });
const imported = await call(auth, 'POST', '/v1/admin/skill-packs/' + encodeURIComponent(pack.id) + '/import', { selected: 'all' });
const imported = await call(auth, 'POST', '/v1/admin/skill-packs/' + encodeURIComponent(pack.id) + '/import', { selected: 'all' }).catch(async (e) => {
const recorded = ((await call(auth, 'GET', '/v1/admin/skill-packs').catch(() => ({}))).packs ?? []).find((p: any) => p.id === pack.id)?.lastImport;
const cap = /pack exceeds max (bytes|files) \(\d+\)/.exec(String(recorded?.error ?? ''));
throw cap ? tooLarge('QM refused the pack: ' + cap[0]) : e;
});
return { pack, existing: !!existing, imported };
};
let out;
+16 -4
View File
@@ -311,7 +311,7 @@ describe('gstack-harness QM (org skill pack, against a fake admin API)', () => {
return dir;
}
test('refuses before sending anything when the repository is over QM\'s pack cap, and names the cap when QM refuses', async () => {
test('refuses before sending anything when the repository is over QM\'s pack cap, and names the cap QM recorded when it refuses, without a retry', async () => {
const q = fakeQm();
try {
const d = tmp();
@@ -322,14 +322,26 @@ describe('gstack-harness QM (org skill pack, against a fake admin API)', () => {
expect(q.calls).toEqual([]);
expect(fs.existsSync(path.join(d, 'state', 'harness'))).toBe(false);
} finally { q.server.stop(true); }
const capped = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname.endsWith('/import')
? new Response('{"error":"internal","message":"pack exceeds max bytes (33554432)"}', { status: 500 })
: req.method === 'GET' ? Response.json({ packs: [] }) : Response.json({ pack: { id: 'pk1' } }) });
const recorded: Array<Record<string, unknown>> = [];
const imports: string[] = [];
const capped = Bun.serve({ port: 0, fetch: async (req) => {
const u = new URL(req.url);
if (req.method === 'GET') return Response.json({ packs: recorded });
if (u.pathname.endsWith('/import')) {
imports.push(u.pathname);
recorded[0].lastImport = { status: 'error', error: 'pack exceeds max bytes (33554432)' };
return Response.json({ error: 'internal', message: 'internal server error' }, { status: 500 });
}
const b = await req.json() as Record<string, unknown>;
recorded.push({ id: 'pk1', url: b.url, ref: b.ref });
return Response.json({ pack: recorded[0] });
} });
try {
const d = tmp();
const err = await registerQm({ checkout: smallCheckout(), stateRoot: path.join(d, 'state'), env: { GSTACK_QM_URL: 'http://127.0.0.1:' + capped.port, GSTACK_QM_ADMIN_TOKEN: 't0k' }, version: VERSION, yes: true }).catch(e => e);
expect(err.code).toBe('HARNESS_QM_PACK_TOO_LARGE');
expect(err.message).toContain('pack exceeds max bytes (33554432)');
expect(imports).toHaveLength(1);
} finally { capped.stop(true); }
});