mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 17:51:58 +01:00
## Thinking Path > - Paperclip manages AI agents and preserves their work history. > - Product E2E cases use TestOps plans to select real AWS evaluations. > - The full chat catalog now includes the former smoke and completion-pilot cases. > - The two old bootstrap workflows can recreate their smaller plans after deletion. > - This pull request removes those workflows and stops their plan-creation functions before API access. > - The full-catalog workflow keeps its shared clients, evidence rules, case identities, and measured results. ## Linked Issues or Issue Description Refs #18542, #18516. The user requested removal of the duplicate smoke and completion-pilot plans and investigation of their recreation paths. Both old workflows accepted manual dispatch only. Their bootstrap operations created plans by name when the plans were absent. They had no schedules in their workflow definitions. The old workflow records were disabled before plan removal. An existing pilot execution was allowed to finish successfully. This change does not cancel executions or delete cases, results, launches, or private evidence. ## What Changed - Remove `chat-feature-testops.yml` and `chat-completion-testops.yml`. - Make the former singleton plan creator reject every call before API reads or mutation markers. - Reject pilot bootstrap before API access. Keep the full-catalog branch and its integration validation. - Remove tests that require the retired workflow files. Keep shared helper and full-catalog controls. Run the trusted-checkout boundary test against the active catalog workflow. - Replace old setup instructions with retirement guidance and the active full-catalog route. The master sync preserves the incoming typed launch-search fix from #18620, including both regression tests. The only conflict was the retired pilot documentation. Its search-format note remains as a retained-helper contract. ## Verification - Two new retirement controls fail against the parent code because it reaches an API or mutation marker. Both pass after the change. - Six owning Node suites after the master sync: 146 passed (144 before the sync). Shared contract: 22 passed. Catalog and retained export controls: 12 passed. Focused workflow and release controls: 7 passed. - Two modified API modules pass Node syntax checks. `git diff --check` passes. - The pure workflow-security file has the same four failures and 13 passes on both the untouched parent and this branch. Two assertions expect eight trusted bootstrap steps where master now has nine. Two lists omit the already-shipped catalog workflow. This change does not repair or relax those unrelated assertions. - A broader release-test invocation had 73 passes and 20 sandbox failures from blocked mock HTTP listeners or npm permissions. Its focused affected workflow control passes. A standard Vitest setup attempt stopped on a missing `acpx/runtime` bundle dependency before tests. No local app, database, provider, or native runtime was qualified. - Current-head PR CI passes: 48 successful jobs and one skipped job, including both required checks. Greptile is 5/5. Independent source review passes. No new model run is needed for this retirement. ## Risks - Existing integrations that point at either retired workflow will stop dispatching. This is the requested behavior; use the full catalog instead. - Historical evidence readers still use shared helpers. Those helpers remain, including the transport and official-context parser used by the active catalog. - Workflow removal is a repository change. TestOps plan deletion and disabling obsolete executors are separate service actions. Neither action should delete shared cases or measurements. ## Model Used OpenAI `gpt-6.1-sol` through Codex, with source editing, shell tools, and independent source review. The context-window size is not asserted. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
284 lines
19 KiB
JavaScript
284 lines
19 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { cases, targetSha, planName } from './chat-completion-testops-contract.mjs';
|
|
import { validatePilotSelection, verifyPilotUpload, bootstrapPilotPlan, completePilotRegistration, launchPilotPlan } from './chat-completion-testops-api.mjs';
|
|
|
|
const selected = cases.map((row, i) => ({ ...row, caseId: 100 + i }));
|
|
const context = { runId: '123', attempt: '1', workflowSha: 'a'.repeat(40), targetSha, operation: 'evaluate' };
|
|
const binding = { planId: 10, jobId: 11, integrationId: 2, cases: selected };
|
|
const env = { ALLURE_JOB_RUN_ID: '77', CHAT_COMPLETION_TESTOPS_BINDING: JSON.stringify(binding) };
|
|
const selection = { schema: 'paperclip.chat-completion-testops-selection/v1', ...context, projectId: 1,
|
|
planId: 10, jobId: 11, allureJobRunId: '77', launchId: 12, cases: selected };
|
|
const runUrl = 'https://github.com/paperclipai/paperclip/actions/runs/123';
|
|
const plan = { id: 10, name: 'Chat completion pilot — OpenCode 1017', projectId: 1, testCasesCount: 3,
|
|
treeSelection: { inverted: false, leafsInclude: selected.map(row => row.caseId) } };
|
|
const job = { id: 11, type: 'github', url: 'https://github.com/paperclipai/paperclip/actions/workflows/chat-completion-testops.yml' };
|
|
const jobrun = { id: 77, job, launchId: 12, stage: 'in_progress', url: runUrl };
|
|
const attachment = { id: 900, name: 'Execution provenance and checks', contentType: 'application/json', contentLength: 100,
|
|
missed: false, fromTestCase: false };
|
|
const screenshot = { ...attachment, id: 901, name: 'Private evidence receipt', contentType: 'application/json', contentLength: 1000 };
|
|
const exported = selected.map(row => ({ caseId: row.caseId, executionId: row.executionId, status: 'passed', cleanup: 'passed',
|
|
attachments: [attachment, screenshot].map(({ id, missed, fromTestCase, ...rest }) => rest) }));
|
|
|
|
// Observed TestOps job-definition carrier: Branch/defaultValue. This is
|
|
// distinct from the id/value carrier accepted by plan runtime overrides.
|
|
const branchDefinition = { id: 34, name: 'Branch', defaultValue: 'refs/heads/master' };
|
|
function mutationApi({ parameters = [branchDefinition], assignments = [], existingLaunch = false, uncertain = false } = {}) {
|
|
const calls = [], current = { ...job, parameters: structuredClone(parameters) };
|
|
let assigned = structuredClone(assignments);
|
|
const api = async (route, method = 'GET', body) => {
|
|
calls.push({ route, method, body });
|
|
if (route === '/api/jobrun/77') return { ...jobrun, stage: 'finished' };
|
|
if (route === '/api/launch/12') return { id: 12, projectId: 1 };
|
|
if (route === '/api/testplan/10') return plan;
|
|
if (route === '/api/job/11') {
|
|
if (method === 'PATCH') {
|
|
assert.deepEqual(body, { integrationId: 2, canRun: true });
|
|
current.canRun = true;
|
|
}
|
|
return structuredClone(current);
|
|
}
|
|
if (route === '/api/testplan/10/job') return structuredClone(assigned);
|
|
if (route === '/api/testplan/10/assign') {
|
|
assert.equal(method, 'POST');
|
|
assert.deepEqual(body, { jobId: 11, selection: { inverted: false, manual: false, leafsInclude: [100, 101, 102] } });
|
|
assigned = [{ job: structuredClone(current), testCasesCount: 3 }]; return null;
|
|
}
|
|
const selectedCase = selected.find(row => route === `/api/testcase/${row.caseId}`);
|
|
if (selectedCase) return { ...selectedCase, id: selectedCase.caseId, projectId: 1, automated: true };
|
|
if (route.startsWith('/api/launch?')) return { content: existingLaunch ? [{ id: 13 }] : [], totalPages: existingLaunch ? 1 : 0 };
|
|
if (route === '/api/testplan/10/run') {
|
|
assert.equal(method, 'POST');
|
|
if (uncertain) throw new Error('uncertain transport');
|
|
return { id: 13, projectId: 1, name: body.launchName };
|
|
}
|
|
assert.fail(route);
|
|
};
|
|
return { api, calls, current };
|
|
}
|
|
const prepared = { planId: 10, integrationId: 2, cases: selected };
|
|
const official = { jobRunId: '77', launchId: 12 };
|
|
const operatorEnv = { CHAT_COMPLETION_TESTOPS_BINDING: JSON.stringify(binding), GITHUB_RUN_NUMBER: '5',
|
|
CHAT_COMPLETION_TESTOPS_LAUNCH_RUN_NUMBER: '5', CHAT_COMPLETION_TESTOPS_LAUNCH_NONCE: 'ab'.repeat(16) };
|
|
|
|
test('registration preserves the observed Branch definition and binds only the owned three-case job', async () => {
|
|
const f = mutationApi(), markers = [];
|
|
const receipt = await completePilotRegistration(f.api, prepared, official, context, 999, async stage => markers.push(stage));
|
|
assert.deepEqual(receipt.binding.cases, selected);
|
|
assert.deepEqual(f.current.parameters, [branchDefinition]);
|
|
assert.deepEqual(markers, ['bind_owned_job', 'assign_plan']);
|
|
assert.deepEqual(f.calls.filter(row => row.method !== 'GET').map(row => row.route), ['/api/job/11', '/api/testplan/10/assign']);
|
|
});
|
|
|
|
for (const parameters of [[], [branchDefinition, { id: 35, name: 'operation', defaultValue: 'bootstrap' }], [{ id: 34, value: 'refs/heads/master' }]]) {
|
|
test(`registration rejects unreviewed parameter carrier/count ${JSON.stringify(parameters)} before writes`, async () => {
|
|
const f = mutationApi({ parameters });
|
|
await assert.rejects(completePilotRegistration(f.api, prepared, official, context, 999, async () => assert.fail('no mutation marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
}
|
|
|
|
for (const assignments of [[{ job: { id: 99 }, testCasesCount: 3 }], [{ job, testCasesCount: 2 }], [{ job, testCasesCount: 3 }, { job, testCasesCount: 3 }]]) {
|
|
test('registration rejects ambiguous assignments before writes', async () => {
|
|
const f = mutationApi({ assignments });
|
|
await assert.rejects(completePilotRegistration(f.api, prepared, official, context, 999, async () => assert.fail('no mutation marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
}
|
|
|
|
test('registration keeps an existing exact assignment without assigning it twice', async () => {
|
|
const f = mutationApi({ assignments: [{ job: { ...job, parameters: [branchDefinition] }, testCasesCount: 3 }] });
|
|
await completePilotRegistration(f.api, prepared, official, context, 999, async () => {});
|
|
assert.equal(f.calls.filter(row => row.route.endsWith('/assign')).length, 0);
|
|
});
|
|
|
|
for (const bucket of [{ job: null, testCasesCount: 3 }, { testCasesCount: 3 }]) {
|
|
test(`registration binds an explicit unassigned bucket (${Object.hasOwn(bucket, 'job') ? 'null' : 'omitted'} job)`, async () => {
|
|
const f = mutationApi({ assignments: [bucket] }), markers = []; let observed;
|
|
await completePilotRegistration(f.api, prepared, official, context, 999, async stage => markers.push(stage), async value => { observed = value; });
|
|
assert.deepEqual(markers, ['bind_owned_job', 'assign_plan']);
|
|
assert.equal(f.calls.filter(row => row.route.endsWith('/assign')).length, 1);
|
|
assert.deepEqual(observed.assignments, [{ kind: 'unassigned', jobId: null, testCasesCount: 3 }]);
|
|
});
|
|
}
|
|
|
|
for (const bucket of [{ job: null, testCasesCount: 2 }, { job: null, testCasesCount: 3, otherJobId: 99 }]) {
|
|
test('registration rejects incomplete or unknown unassigned bucket data without writes', async () => {
|
|
const f = mutationApi({ assignments: [bucket] });
|
|
await assert.rejects(completePilotRegistration(f.api, prepared, official, context, 999, async () => assert.fail('no mutation marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
}
|
|
|
|
test('bootstrap retains a closed observation before rejecting an unknown parameter shape', async () => {
|
|
const f = mutationApi({ parameters: [{ id: 34, name: 'PRIVATE_NAME', defaultValue: 'PRIVATE_DEFAULT', value: 'PRIVATE_VALUE' }] });
|
|
let observed;
|
|
await assert.rejects(completePilotRegistration(f.api, prepared, official, context, 999, async () => assert.fail('no mutation marker'), async value => { observed = value; }));
|
|
assert.equal(observed.qualification, 'inspection_only'); assert.equal(observed.jobId, 11);
|
|
assert.equal(observed.parameters[0].runtimeValuePresent, true);
|
|
assert.ok(!JSON.stringify(observed).includes('PRIVATE_'));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
|
|
test('the fenced launcher requests one exact three-case TestOps launch', async () => {
|
|
const f = mutationApi({ assignments: [{ job, testCasesCount: 3 }] }), markers = [];
|
|
const receipt = await launchPilotPlan(f.api, operatorEnv, context, 999, async stage => markers.push(stage));
|
|
assert.deepEqual(receipt.caseIds, [100, 101, 102]); assert.deepEqual(markers, ['launch_plan']);
|
|
assert.deepEqual(f.calls.filter(row => row.method !== 'GET'), [{ route: '/api/testplan/10/run', method: 'POST', body: {
|
|
launchName: `${planName} · ${operatorEnv.CHAT_COMPLETION_TESTOPS_LAUNCH_NONCE}` } }]);
|
|
});
|
|
|
|
test('completion launch preflight sends the typed UTF-8 name filter required by TestOps', async () => {
|
|
const f = mutationApi({ assignments: [{ job, testCasesCount: 3 }] });
|
|
const api = async (route, ...args) => {
|
|
if (route.startsWith('/api/launch?')) {
|
|
const wire = new URL(route, 'https://paperclip.testops.cloud').searchParams.get('search');
|
|
let filters;
|
|
try { filters = JSON.parse(Buffer.from(wire, 'base64').toString('utf8')); }
|
|
catch { throw new Error('TestOps HTTP 400: typed launch search required'); }
|
|
assert.deepEqual(filters, [{ id: 'name', type: 'string', value: `${planName} · ${operatorEnv.CHAT_COMPLETION_TESTOPS_LAUNCH_NONCE}` }]);
|
|
}
|
|
return f.api(route, ...args);
|
|
};
|
|
await launchPilotPlan(api, operatorEnv, context, 999, async () => {});
|
|
assert.equal(f.calls.filter(row => row.method === 'POST').length, 1);
|
|
});
|
|
|
|
for (const changed of [{ GITHUB_RUN_NUMBER: '6' }, { CHAT_COMPLETION_TESTOPS_LAUNCH_NONCE: 'not-a-nonce' }, { CHAT_COMPLETION_TESTOPS_LAUNCH_RUN_NUMBER: '' }]) {
|
|
test(`launcher rejects its invalid fence before requests: ${Object.keys(changed)[0]}`, async () => {
|
|
const f = mutationApi();
|
|
await assert.rejects(launchPilotPlan(f.api, { ...operatorEnv, ...changed }, context, 999, async () => assert.fail('no marker')));
|
|
assert.equal(f.calls.length, 0);
|
|
});
|
|
}
|
|
|
|
test('launcher reconciles an existing nonce without requesting a second launch', async () => {
|
|
const f = mutationApi({ assignments: [{ job, testCasesCount: 3 }], existingLaunch: true });
|
|
await assert.rejects(launchPilotPlan(f.api, operatorEnv, context, 999, async () => assert.fail('no marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
|
|
for (const assignments of [[], [{ job: { id: 99 }, testCasesCount: 3 }], [{ job, testCasesCount: 2 }]]) {
|
|
test('launcher rejects an unbound or mismatched assignment without requesting work', async () => {
|
|
const f = mutationApi({ assignments });
|
|
await assert.rejects(launchPilotPlan(f.api, operatorEnv, context, 999, async () => assert.fail('no mutation marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
}
|
|
|
|
test('launcher rejects a changed default branch before requesting work', async () => {
|
|
const f = mutationApi({ assignments: [{ job, testCasesCount: 3 }], parameters: [{ ...branchDefinition, defaultValue: 'refs/heads/candidate' }] });
|
|
await assert.rejects(launchPilotPlan(f.api, operatorEnv, context, 999, async () => assert.fail('no mutation marker')));
|
|
assert.equal(f.calls.filter(row => row.method !== 'GET').length, 0);
|
|
});
|
|
|
|
test('an uncertain launch response is never automatically resent', async () => {
|
|
const f = mutationApi({ assignments: [{ job, testCasesCount: 3 }], uncertain: true }), seen = new Set();
|
|
const mark = async stage => { if (seen.has(stage)) throw new Error('already attempted'); seen.add(stage); };
|
|
await assert.rejects(launchPilotPlan(f.api, operatorEnv, context, 999, mark), /uncertain transport/);
|
|
await assert.rejects(launchPilotPlan(f.api, operatorEnv, context, 999, mark), /already attempted/);
|
|
assert.equal(f.calls.filter(row => row.method === 'POST').length, 1);
|
|
});
|
|
|
|
test('retired pilot bootstrap cannot recreate a plan or call the integration API', async () => {
|
|
await assert.rejects(bootstrapPilotPlan(() => assert.fail('retired route must not call the API'), 2,
|
|
() => assert.fail('retired route must not mark a mutation')), /chat_completion_testops_plan_retired/);
|
|
});
|
|
|
|
function api(replace = {}) {
|
|
const fixtures = {
|
|
'/api/testplan/10': plan, '/api/testplan/10/job': [{ job, testCasesCount: 3 }], '/api/job/11': job,
|
|
'/api/jobrun/77': jobrun, '/api/launch/12': { id: 12, projectId: 1, closed: false },
|
|
'/api/jobrun/77/plan?expected=true': selected.map(row => ({ id: row.caseId, selector: row.fullName })),
|
|
'/api/testresult?launchId=12&page=0&size=100': { content: selected.map((row, i) => ({ id: 200 + i, testCaseId: row.caseId })), totalElements: 3, totalPages: 1 },
|
|
};
|
|
for (const [i, row] of selected.entries()) {
|
|
fixtures[`/api/testcase/${row.caseId}`] = { id: row.caseId, fullName: row.fullName, projectId: 1, automated: true };
|
|
fixtures[`/api/testresult/${200 + i}`] = { id: 200 + i, testCaseId: row.caseId, fullName: row.fullName,
|
|
projectId: 1, launchId: 12, status: 'passed', links: [], jobRun: { id: 77, launchId: 12, url: runUrl, job } };
|
|
fixtures[`/api/testresult/attachment?testResultId=${200 + i}&page=0&size=100`] = { content: [attachment, screenshot], totalElements: 2, totalPages: 1 };
|
|
}
|
|
return async (route, method = 'GET') => {
|
|
assert.equal(method, 'GET'); assert.ok(Object.hasOwn(fixtures, route), route);
|
|
return typeof replace[route] === 'function' ? replace[route]() : structuredClone(replace[route] ?? fixtures[route]);
|
|
};
|
|
}
|
|
|
|
test('validates actual registered plan, job run and all three selected case identities', async () => {
|
|
const value = await validatePilotSelection(api(), env, context, 999);
|
|
assert.deepEqual(value.cases.map(row => row.executionId), cases.map(row => row.executionId));
|
|
assert.equal(value.targetSha, targetSha);
|
|
});
|
|
|
|
test('a genuine one-case selection executes only that case', async () => {
|
|
const value = await validatePilotSelection(api({ '/api/jobrun/77/plan?expected=true': [{ id: selected[2].caseId, selector: selected[2].fullName }] }), env, context, 999);
|
|
assert.deepEqual(value.cases.map(row => row.key), ['work']);
|
|
});
|
|
|
|
for (const kind of ['foreign-case', 'wrong-selector', 'duplicate-case', 'wrong-workflow', 'foreign-project', 'wrong-jobrun', 'wrong-plan-job']) {
|
|
test(`stops ${kind} before any provider work`, async () => {
|
|
const replacements = {};
|
|
if (kind === 'foreign-case') replacements['/api/jobrun/77/plan?expected=true'] = [{ id: 999, selector: selected[0].fullName }];
|
|
if (kind === 'wrong-selector') replacements['/api/jobrun/77/plan?expected=true'] = [{ id: 100, selector: selected[1].fullName }];
|
|
if (kind === 'duplicate-case') replacements['/api/jobrun/77/plan?expected=true'] = [{ id: 100, selector: selected[0].fullName }, { id: 100, selector: selected[0].fullName }];
|
|
if (kind === 'wrong-workflow') replacements['/api/job/11'] = { ...job, url: job.url.replace('chat-completion', 'foreign') };
|
|
if (kind === 'foreign-project') replacements['/api/launch/12'] = { id: 12, projectId: 2 };
|
|
if (kind === 'wrong-jobrun') replacements['/api/jobrun/77'] = { ...jobrun, url: runUrl + '4' };
|
|
if (kind === 'wrong-plan-job') replacements['/api/testplan/10/job'] = [{ job: { id: 12 }, testCasesCount: 3 }];
|
|
await assert.rejects(validatePilotSelection(api(replacements), env, context, 999));
|
|
});
|
|
}
|
|
|
|
test('verifies exact results and attachment metadata for all three cases', async () => {
|
|
const value = await verifyPilotUpload(api(), selection, context, exported);
|
|
assert.equal(value.verification, 'VERIFIED'); assert.equal(value.results.length, 3);
|
|
assert.ok(value.results.every(row => row.attachmentsVerified && row.status === 'passed'));
|
|
});
|
|
|
|
test('accepts a finished TestOps job with its original complete results', async () => {
|
|
const value = await verifyPilotUpload(api({ '/api/jobrun/77': { ...jobrun, stage: 'finished' } }), selection, context, exported);
|
|
assert.equal(value.verification, 'VERIFIED');
|
|
});
|
|
for (const stage of ['cancelled', 'failed', 'scheduled']) {
|
|
test(`never qualifies a ${stage} TestOps job even with all results and attachments`, async () => {
|
|
await assert.rejects(verifyPilotUpload(api({ '/api/jobrun/77': { ...jobrun, stage } }), selection, context, exported));
|
|
});
|
|
}
|
|
|
|
test('waits for an explicitly missing attachment and requires final complete metadata', async () => {
|
|
let polls = 0, slept = 0, clock = 0;
|
|
const get = api({ '/api/testresult/attachment?testResultId=200&page=0&size=100': () => {
|
|
polls++;
|
|
return { content: [{ ...attachment, missed: polls === 1 }, screenshot], totalElements: 2, totalPages: 1 };
|
|
} });
|
|
const value = await verifyPilotUpload(get, selection, context, exported, { now: () => clock, wait: async ms => { slept++; clock += ms; } });
|
|
assert.equal(value.verification, 'VERIFIED'); assert.equal(polls, 2); assert.equal(slept, 1);
|
|
});
|
|
|
|
test('permanently missing or inherited attachments never qualify', async () => {
|
|
for (const field of ['missed', 'fromTestCase']) {
|
|
let clock = 0;
|
|
const get = api({ '/api/testresult/attachment?testResultId=200&page=0&size=100': { content: [{ ...attachment, [field]: true }, screenshot], totalElements: 2, totalPages: 1 } });
|
|
await assert.rejects(verifyPilotUpload(get, selection, context, exported, { now: () => clock, wait: async ms => { clock += ms; } }));
|
|
assert.ok(clock < 120_000);
|
|
}
|
|
});
|
|
|
|
for (const kind of ['wrong-case', 'wrong-source-run', 'wrong-status', 'foreign-result', 'duplicate-result', 'extra-result', 'foreign-embedded-job', 'foreign-source-link']) {
|
|
test(`preserves failure for ${kind} during server result readback`, async () => {
|
|
const replacements = {};
|
|
if (kind === 'wrong-case') replacements['/api/testresult/200'] = { id: 200, testCaseId: 999 };
|
|
if (kind === 'wrong-source-run') replacements['/api/jobrun/77'] = { ...jobrun, url: runUrl + '4' };
|
|
if (kind === 'wrong-status') replacements['/api/testresult/200'] = { id: 200, testCaseId: 100, fullName: selected[0].fullName,
|
|
projectId: 1, launchId: 12, status: 'failed', links: [], jobRun: { id: 77, launchId: 12, url: runUrl, job } };
|
|
if (kind === 'foreign-embedded-job' || kind === 'foreign-source-link') replacements['/api/testresult/200'] = {
|
|
id: 200, testCaseId: 100, fullName: selected[0].fullName, projectId: 1, launchId: 12, status: 'passed',
|
|
links: kind === 'foreign-source-link' ? [{ url: 'https://github.com/paperclipai/paperclip/commit/' + 'b'.repeat(40) }] : [],
|
|
jobRun: { id: 77, launchId: 12, url: runUrl, job: kind === 'foreign-embedded-job' ? { ...job, type: 'foreign' } : job } };
|
|
if (kind === 'foreign-result') replacements['/api/testresult?launchId=12&page=0&size=100'] = { content: [{ id: 999, testCaseId: 999 }], totalElements: 1, totalPages: 1 };
|
|
if (kind === 'duplicate-result') replacements['/api/testresult?launchId=12&page=0&size=100'] = { content: [{ id: 200, testCaseId: 100 }, { id: 200, testCaseId: 100 }], totalElements: 2, totalPages: 1 };
|
|
if (kind === 'extra-result') replacements['/api/testresult?launchId=12&page=0&size=100'] = { content: selected.map((row, i) => ({ id: 200 + i, testCaseId: row.caseId })).concat({ id: 999, testCaseId: 999 }), totalElements: 4, totalPages: 1 };
|
|
await assert.rejects(verifyPilotUpload(api(replacements), selection, context, exported));
|
|
});
|
|
}
|