Skip to content

Commit d5151a7

Browse files
fix(rest)!: an async import job whose every row failed ends failed, not succeeded (#27795)
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_ Fixes #27676 domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: none — no hand-written docs page or published skill states the async import job's terminal-status rule (`git grep` of `content/docs` outside `references/`/`releases/` and of `skills/` for the job status vocabulary: 0 hits; `import-mappings.mdx` documents per-row outcomes only). The `ImportJobStatus` docblock and the generated reference are the spec seat's card #27734. ## 维护者速读 - 改了什么:后台导入作业(`POST /data/OBJ/import/jobs`)在未取消、有行、且没有任何一行被新建/更新/跳过时,终态记为 `failed`(原来是 `succeeded`),`error` 写一句话说明每行都失败、去看 `results`。 - 为什么改:按你的裁决 B(记录 6509036166):一行都没导入的作业不能对只看 `status` 的客户端说「成功」。 - 风险与回滚:只改这一种终态;部分失败、全部跳过、零行作业仍是 `succeeded`,取消仍优先;撤销不受影响(这种作业本来就不可撤销)。回滚即删掉 `data-action-routes.ts` 里的一个判断。 - 先看:`packages/rest/src/data-action-routes.ts` 里 `everyRowFailed` 那段;`packages/rest/src/import-job-integration.test.ts` 末尾新增的 describe;`packages/rest/src/import-row-cell-errors.test.ts` 的轮询期望。 - 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run src/import-job-integration.test.ts`,预期 21/21 通过。 - 需要你做的:无,席位落地。 ## Ruling (binding) Maintainer ruling B, record 6509036166 on #27676, execution parameters quoted: > 1. **Predicate.** At the terminal-status line of the job worker (`packages/rest/src/data-action-routes.ts`, the `finalStatus` computation): `!cancelled && total > 0 && created + updated + skipped === 0` ⇒ `failed`, else today's rule. Not `ok === 0`: a skipped row is not counted in `ok`. `error` is set to one sentence saying every row failed and pointing at `results`, with no tracker number. > 2. **Kept as `succeeded` on purpose:** a zero-row job, an all-skipped job, and a partly-failed job. The batch door's "any failure ⇒ false" rule is not carried over. Cancellation still wins. > 3. **Dry-run:** the same rule (an all-error dry-run ends `failed`). > 4. **Server PR** (`domain:server`, after #27501 (a) PR #27711 merges; same file), with five pins: all rows fail → `failed`, `errors === total`, `error` present; 2 ok + 1 fail → `succeeded` (the existing partial pin is unchanged); all skipped → `succeeded`; cancellation still wins; all-error dry-run → `failed`. > 5. **Spec docblock** (`packages/spec/src/api/export.zod.ts`, the `ImportJobStatus` / `ImportJobProgressSchema.error` sentences, plus `gen:docs`) goes to the `domain:spec` seat as its own PR, not in the server PR. ## What changes - `packages/rest/src/data-action-routes.ts`, the async import worker's terminal status. The existing cancel resolution runs first and is unchanged: `summary.cancelled`, then the in-memory `cancelledImportJobs` flag, then the durable row re-read. Only a status still `succeeded` after all three is tested by the new predicate, so a cancel always wins, including a late one. - The predicate, mapped onto the runner's real summary (`ImportRunSummary extends ImportProgress`, `packages/core/src/utils/import-runner.ts`): ruling `cancelled` = the resolved status is not `cancelled` (`summary.cancelled` plus the two late re-checks); `total` = `summary.total` (`rows.length`); `created` / `updated` / `skipped` = `summary.created` / `summary.updated` / `summary.skipped`. `summary.ok` is not read: it counts created and updated rows only. - When the predicate holds, the final patch writes `status: 'failed'` and `error`: "Every row of this import failed and nothing was imported; each row's reason is in the job's results." Counts, `results` and `completed_at` are written exactly as before. A dry run takes the same path (its valid rows count as created / updated in the runner's preview). - No `packages/spec` change: the `ImportJobStatus` / `error` docblocks are #27734. - Changeset: `@objectstack/rest` patch, BREAKING note, ADR-0087 `not-required (no-migration-prescription)` marker. ### Readers of `failed` vs `succeeded` in this lane, measured - Undo (`importJobUndoable`, `rest-server.ts`): admits `succeeded` / `cancelled` only, and also needs a non-empty undo log. A job that created and updated nothing has an empty log, so it was not undoable before and is not now. Pinned: the all-failed job reads `undoable: false` and undo answers 422 `NOT_UNDOABLE`. - Cancel route: acts on `pending` / `running` only; unaffected. - History filter `?status=`: passes the string through; pinned that the all-failed job lists under `status=failed`. - Console Import Wizard (`ImportWizard.tsx` `importResultOutcome`): already reads "nothing imported and something errored" as `failed`, and `jobStatus === 'failed'` as `failed`; the synthetic row at the `prog.status === 'failed'` branch fires only when no per-row error arrived, which this change never produces. `failed` is already in its closed terminal set. No UI change. ## Tests `packages/rest/src/import-job-integration.test.ts`, a new describe on the file's harness (real `RestServer` route, real `ObjectQL` engine + `ObjectStackProtocolImplementation`, SQLite `:memory:`, real `sys_import_job`): - every row fails → `failed`, `errors === total` (3), `error` present and naming `results`; `results` has 3 failed rows; nothing landed; `undoable: false`, undo 422 `NOT_UNDOABLE`; listed under `status=failed`. - control: every row skipped (`writeMode: 'update'`, no match) → `succeeded`, no `error`. - control: a header-only CSV (0 rows) → `succeeded`, no `error`. - every row of a dry run fails → `failed`, `dryRun: true`. - control: a valid dry run → `succeeded`. - control: a cancel that lands after the last row of a job whose every row fails at the driver → `cancelled`, with `errors: 3` (it waits for the worker's own final write of `results` before reading). - 2 ok + 1 fail → `succeeded`: the existing first case of the file, unchanged. `packages/rest/src/import-row-cell-errors.test.ts`: its async poll helper asserted `succeeded` for every job; the two one-row jobs whose only row fails (`NO_NAME`) now expect `failed`, per the ruling. A stale fixture, a direct consequence of this change. ## Acceptance notes - The pins drive the route handlers registered on a real `RestServer`, not an HTTP transport, the harness the file's existing cases use. - The first full-suite run showed the late-cancel pin racing a fixed 50 ms wait under load (it read the cancel route's write before the worker's final patch); the pin now waits on the worker's `results` write instead. Re-run green in the full suite. - The sync door (`POST /data/:object/import`) has no status and is untouched. ## Verification Readings are at head `2fa11634` unless a line names another sha. <details><summary>Red before the fix (e536326, tests only)</summary> ``` × every row fails: the job ends failed, errors === total, and error is set × every row of a dry run fails: the job ends failed - "status": "failed", + "status": "succeeded", Tests 2 failed | 19 passed (21) ``` </details> <details><summary>Ablations (scripts/ablation-replace.mjs; each restored blob == HEAD, `git diff HEAD` empty)</summary> The suites import `./rest-server` from source, so no dist rebuild was involved. 1. Delete `if (everyRowFailed) finalStatus = 'failed';` → red: the two all-failed pins plus the two `import-row-cell-errors` polls (`Tests 4 failed | 23 passed (27)`). 2. Predicate spelled `summary.ok === 0` → red: "every row skipped keeps succeeded" (`expected "succeeded", received "failed"`). 3. Cancel guard spelled `!summary.cancelled` (ignoring the late re-checks) → red: the late-cancel control. 4. `&& summary.total > 0` deleted → red: "a job with no rows keeps succeeded". (A first attempt with an empty `--replacement` was refused by the tool before running anything; re-run with `--delete`.) </details> <details><summary>Package suites and gates</summary> - `pnpm --filter @objectstack/rest test`: lock VERDICT command-exit 0, 342/342 files, 6701 passed, 326 skipped. - `pnpm --filter @objectstack/rest typecheck`: exit 0, 0 test-layer errors (at 138a620; the commit after it touches two test files that the full suite above compiled and ran). - `eslint packages/rest --no-inline-config`: exit 0. - `dispatch-gates --commands`: 59 families derived; results in the os-dev-report on #27676. `check:dual-build-cjs-loads` exits 3 PREREQUISITE NOT MET (it reads every package's dist): NOT MEASURED locally, carried by CI `Lint & Repo Gates`. - No open PR touches `data-action-routes.ts`, `import-job-integration.test.ts` or `import-row-cell-errors.test.ts` (REST read of all 35 open PRs' files). - Dogfood battery (`(narrowing)` arm) at the final head `2c10836a`: os-verify-lock VERDICT exit 0 (held 31m29s), 355 files passed and 3 skipped (358), 2867 tests passed and 20 skipped (2887), no failures (os-dev-report 6512268257). An earlier run at `2fa11634` had 2 failures, both the stale dogfood poll in `packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts`, which `2c10836a` updates (it waited for `succeeded` on an all-rows-failed job); that file is added to the claim by amendment. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ae6384 commit d5151a7

5 files changed

Lines changed: 175 additions & 7 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
fix(rest)!: an async import job whose every row failed ends `failed`, not `succeeded` (#27676)
6+
7+
Clause-②: no (narrowing) — the async import job's `succeeded` status is no longer emitted for a job that imported nothing; it now reads `failed`. No value, key or shape is added.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: one already-declared status value (`failed`) is now emitted for a job that created, updated and skipped no row. There is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** a published status value changes meaning at one REST door (`@objectstack/rest`), per maintainer ruling B (record 6509036166).
13+
14+
- **Before.** A job of `POST /data/:object/import/jobs` that was not cancelled and did not throw ended `succeeded`, even when every row failed and nothing was written. The only sign was `errors` equal to `total`.
15+
- **Now.** A job that was not cancelled, had at least one row, and created, updated or skipped none of them ends `failed`. Its `error` says every row failed and points at the job's `results`, which still carry each row's reason. A dry run follows the same rule: a dry run whose every row is invalid ends `failed`.
16+
- **Unchanged, on purpose.** A job whose rows partly failed still ends `succeeded`. So does a job whose every row was skipped (for example `writeMode: 'update'` with no match), and a job with no rows. A cancel still wins: a cancelled job ends `cancelled` whatever its rows did. Counts and `results` are written as before.
17+
- **Who sees it.** A client that polls an import job and branches on `status === 'succeeded'` is now told `failed` for a job that imported nothing. Undo is unaffected: such a job wrote nothing, so it was never undoable.
18+
19+
**The remedy.** Read `failed` as "this job imported nothing"; read `errors` and `results` for why. A client that wants to treat such a job as finished can treat `succeeded` and `failed` alike as terminal, as before.

‎packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ describe('[#22739] an import matches a lookup cell only against a target whose d
8383
const { jobId } = (await res.json()) as { jobId: string };
8484
for (let i = 0; i < 200; i++) {
8585
const body: any = await (await stack.apiAs(who, 'GET', `/data/import/jobs/${jobId}/results`)).json();
86-
if (body.status === 'succeeded') return body.results;
86+
// A job whose every row failed ends `failed`, one with any row ok
87+
// ends `succeeded` (maintainer ruling B, record 6509036166).
88+
if (body.status === 'succeeded' || body.status === 'failed') {
89+
expect(body.status, JSON.stringify(body)).toBe((body.results as Row[]).some((r) => r.ok) ? 'succeeded' : 'failed');
90+
return body.results;
91+
}
8792
expect(['pending', 'running'], JSON.stringify(body)).toContain(body.status);
8893
await new Promise((r) => setTimeout(r, 25));
8994
}

‎packages/rest/src/data-action-routes.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ function describeNonFieldMap(value: unknown): string {
179179
return `a ${typeof value}`;
180180
}
181181

182+
/**
183+
* The `error` of an async import job that ended `failed` because every row
184+
* failed (maintainer ruling B, record 6509036166): the per-row reasons are the
185+
* job's `results`, which this sentence points the reader at.
186+
*/
187+
const IMPORT_JOB_EVERY_ROW_FAILED =
188+
'Every row of this import failed and nothing was imported; each row\'s reason is in the job\'s results.';
189+
182190
/** [#27500] The formats `GET /data/:object/export` serves, csv first as the default. */
183191
const EXPORT_FORMATS = ['csv', 'json', 'xlsx'] as const;
184192
type ExportFormatServed = typeof EXPORT_FORMATS[number];
@@ -762,16 +770,28 @@ export function registerDataActionEndpoints(this: DataActionRouteHost, basePath:
762770
// the durable row 'cancelled', and a late 'succeeded' here
763771
// would silently overwrite it (framework#2824). Counts stay
764772
// truthful either way — they reflect what was written.
765-
let finalStatus = summary.cancelled ? 'cancelled' : 'succeeded';
773+
let finalStatus: 'cancelled' | 'succeeded' | 'failed' = summary.cancelled ? 'cancelled' : 'succeeded';
766774
if (finalStatus === 'succeeded' && this.cancelledImportJobs.has(jobId)) finalStatus = 'cancelled';
767775
if (finalStatus === 'succeeded') {
768776
try {
769777
const row = await loadImportJob(p, jobId, environmentId, context);
770778
if (String(row?.status ?? '') === 'cancelled') finalStatus = 'cancelled';
771779
} catch { /* keep succeeded */ }
772780
}
781+
// A job that was not cancelled, had rows, and created,
782+
// updated or skipped none of them accomplished nothing: it
783+
// ends `failed`, not `succeeded` (maintainer ruling B,
784+
// record 6509036166). Decided after every cancel re-check
785+
// above, so a cancel still wins. `ok` is not the test: a
786+
// skipped row is not counted in it. A zero-row job, an
787+
// all-skipped job and a partly-failed job stay `succeeded`
788+
// on purpose; a dry run follows the same rule.
789+
const everyRowFailed = finalStatus === 'succeeded' && summary.total > 0
790+
&& summary.created + summary.updated + summary.skipped === 0;
791+
if (everyRowFailed) finalStatus = 'failed';
773792
await patch({
774793
status: finalStatus,
794+
...(everyRowFailed ? { error: IMPORT_JOB_EVERY_ROW_FAILED } : {}),
775795
processed_rows: summary.processed,
776796
created_count: summary.created,
777797
updated_count: summary.updated,

‎packages/rest/src/import-job-integration.test.ts‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,123 @@ describe('async import job — an unregistered object is refused before a job ex
508508
expect(rows[0]).toMatchObject({ id: res._json.jobId, object_name: 'task' });
509509
});
510510
});
511+
512+
/**
513+
* The terminal status of a job that wrote nothing (maintainer ruling B, record
514+
* 6509036166). A job that was not cancelled, had at least one row, and ended
515+
* with no row created, updated or skipped ends `failed`, with `error` naming
516+
* the outcome and pointing at `results`. Kept `succeeded` on purpose: a job
517+
* whose rows partly failed (the partial pin in the first describe above), a job
518+
* whose every row was skipped, and a job with no rows. Cancellation still wins.
519+
* A dry run follows the same rule.
520+
*/
521+
describe('async import job — a job whose every row failed ends failed', () => {
522+
let ctx: Awaited<ReturnType<typeof boot>>;
523+
beforeEach(async () => {
524+
ctx = await boot();
525+
expect(ctx.create && ctx.progress && ctx.results && ctx.undo).toBeTruthy();
526+
});
527+
528+
const badRows = (prefix: string) => [
529+
{ id: `${prefix}1`, title: 'one', score: 'not-a-number' },
530+
{ id: `${prefix}2`, title: 'two', score: 'nope' },
531+
{ id: `${prefix}3`, title: 'three', score: 'x' },
532+
];
533+
534+
it('every row fails: the job ends failed, errors === total, and error is set', async () => {
535+
const res = await callCreate(ctx.create, { format: 'json', rows: badRows('af') });
536+
expect(res._status).toBe(201);
537+
const done = await waitForTerminal(ctx.progress, res._json.jobId);
538+
expect(done).toMatchObject({ status: 'failed', total: 3, processed: 3, created: 0, updated: 0, skipped: 0, errors: 3 });
539+
expect(done.errors).toBe(done.total);
540+
expect(typeof done.error).toBe('string');
541+
expect(done.error).toMatch(/results/);
542+
// The per-row report is still delivered beside the status.
543+
const results = await callJob(ctx.results, res._json.jobId);
544+
expect(results._json.results).toHaveLength(3);
545+
expect(results._json.results.every((r: any) => r.ok === false)).toBe(true);
546+
// Nothing landed, and the undo reader answers as it did for this job before.
547+
expect(await ctx.engine.find('task', { where: {} })).toHaveLength(0);
548+
expect(done.undoable).toBe(false);
549+
const u = await callJob(ctx.undo, res._json.jobId);
550+
expect(u._status).toBe(422);
551+
expect(u._json.code).toBe('NOT_UNDOABLE');
552+
// The history filter sees it under its real status.
553+
const failedOnly = await callJob(ctx.list, '', { status: 'failed' });
554+
expect(failedOnly._json.jobs.map((j: any) => j.jobId)).toEqual([res._json.jobId]);
555+
});
556+
557+
it('control: every row skipped keeps succeeded', async () => {
558+
const res = await callCreate(ctx.create, {
559+
format: 'json', writeMode: 'update', matchFields: ['id'],
560+
rows: [{ id: 'sk1', title: 'a' }, { id: 'sk2', title: 'b' }],
561+
});
562+
expect(res._status).toBe(201);
563+
const done = await waitForTerminal(ctx.progress, res._json.jobId);
564+
expect(done).toMatchObject({ status: 'succeeded', total: 2, created: 0, updated: 0, skipped: 2, errors: 0 });
565+
expect(done.error).toBeUndefined();
566+
});
567+
568+
it('control: a job with no rows keeps succeeded', async () => {
569+
const res = await callCreate(ctx.create, { format: 'csv', csv: 'id,title\n' });
570+
expect(res._status).toBe(201);
571+
expect(res._json.total).toBe(0);
572+
const done = await waitForTerminal(ctx.progress, res._json.jobId);
573+
expect(done).toMatchObject({ status: 'succeeded', total: 0, errors: 0 });
574+
expect(done.error).toBeUndefined();
575+
});
576+
577+
it('every row of a dry run fails: the job ends failed', async () => {
578+
const res = await callCreate(ctx.create, { format: 'json', dryRun: true, rows: badRows('dr') });
579+
expect(res._status).toBe(201);
580+
const done = await waitForTerminal(ctx.progress, res._json.jobId);
581+
expect(done).toMatchObject({ status: 'failed', dryRun: true, total: 3, created: 0, updated: 0, skipped: 0, errors: 3 });
582+
expect(typeof done.error).toBe('string');
583+
});
584+
585+
it('control: a valid dry run keeps succeeded', async () => {
586+
const res = await callCreate(ctx.create, { format: 'json', dryRun: true, rows: [{ id: 'dv1', title: 'ok' }] });
587+
const done = await waitForTerminal(ctx.progress, res._json.jobId);
588+
expect(done).toMatchObject({ status: 'succeeded', dryRun: true, created: 1, errors: 0 });
589+
});
590+
591+
it('control: a cancel that lands after the last row still wins over an all-failed outcome', async () => {
592+
// Every row collides with a seeded primary key, so each fails at the
593+
// driver; the slowed writes let the cancel land while the job runs, and
594+
// the 3-row job has no mid-loop checkpoint, so the loop finishes first.
595+
const slowCtx = await boot((driver) => {
596+
const bulkCreate = driver.bulkCreate.bind(driver);
597+
driver.bulkCreate = async (o: string, rows2: any[], ...rest: any[]) => {
598+
if (o === 'task') await new Promise((r) => setImmediate(r));
599+
return bulkCreate(o, rows2, ...rest);
600+
};
601+
const create = driver.create.bind(driver);
602+
driver.create = async (o: string, data: any, ...rest: any[]) => {
603+
if (o === 'task') await new Promise((r) => setImmediate(r));
604+
return create(o, data, ...rest);
605+
};
606+
});
607+
for (const id of ['cf1', 'cf2', 'cf3']) {
608+
await slowCtx.protocol.createData({ object: 'task', data: { id, title: 'seeded' } });
609+
}
610+
const created = await callCreate(slowCtx.create, {
611+
format: 'json',
612+
rows: [{ id: 'cf1', title: 'a' }, { id: 'cf2', title: 'b' }, { id: 'cf3', title: 'c' }],
613+
});
614+
const jobId = created._json.jobId;
615+
const c = await callJob(slowCtx.cancel, jobId);
616+
expect(c._json).toMatchObject({ success: true });
617+
618+
await waitForTerminal(slowCtx.progress, jobId);
619+
// The cancel route writes `cancelled` first; wait until the worker's own
620+
// final patch has landed (it is the only write of `results`), then read the row.
621+
for (let i = 0; i < 400; i++) {
622+
const r = await callJob(slowCtx.results, jobId);
623+
if ((r._json?.results ?? []).length === 3) break;
624+
await new Promise((rr) => setTimeout(rr, 5));
625+
}
626+
const after = await callJob(slowCtx.progress, jobId);
627+
expect(after._json.status).toBe('cancelled');
628+
expect(after._json).toMatchObject({ processed: 3, created: 0, updated: 0, skipped: 0, errors: 3 });
629+
});
630+
});

‎packages/rest/src/import-row-cell-errors.test.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,12 @@ async function boot() {
106106
return r.body.results as any[];
107107
};
108108

109-
/** The async job's per-row report, read off the results route once the job is terminal. */
110-
const jobRows = async (rows: Array<Record<string, unknown>>, dryRun: boolean) => {
109+
/**
110+
* The async job's per-row report, read off the results route once the job is terminal.
111+
* A job whose every row failed ends `failed` (maintainer ruling B, record 6509036166);
112+
* one with any row created, updated or skipped ends `succeeded`.
113+
*/
114+
const jobRows = async (rows: Array<Record<string, unknown>>, dryRun: boolean, terminal: 'succeeded' | 'failed' = 'succeeded') => {
111115
const created = await call('POST', '/api/v1/data/:object/import/jobs', { format: 'json', rows, dryRun });
112116
expect(created.status, JSON.stringify(created.body)).toBe(201);
113117
const jobId = String(created.body.jobId);
@@ -117,7 +121,7 @@ async function boot() {
117121
if (['succeeded', 'failed', 'cancelled'].includes(progress.body?.status)) break;
118122
await new Promise((r) => setTimeout(r, 5));
119123
}
120-
expect(progress?.body?.status, JSON.stringify(progress?.body)).toBe('succeeded');
124+
expect(progress?.body?.status, JSON.stringify(progress?.body)).toBe(terminal);
121125
const results = await call('GET', '/api/v1/data/import/jobs/:jobId/results', undefined, { jobId });
122126
return [...(results.body.results as any[])].sort((a, b) => a.row - b.row);
123127
};
@@ -174,15 +178,15 @@ describe('[#25582] a row that failed for another reason carries no `errors`', ()
174178

175179
it('the dry run\'s validate verdict (sync and async)', async () => {
176180
const b = await boot();
177-
for (const rows of [await b.syncRows([NO_NAME], true), await b.jobRows([NO_NAME], true)]) {
181+
for (const rows of [await b.syncRows([NO_NAME], true), await b.jobRows([NO_NAME], true, 'failed')]) {
178182
expect(rows[0], JSON.stringify(rows[0])).toMatchObject({ row: 1, ok: false, action: 'failed', field: 'name', code: 'required' });
179183
expect(rows[0]).not.toHaveProperty('errors');
180184
}
181185
});
182186

183187
it('the commit\'s refused write (sync and async)', async () => {
184188
const b = await boot();
185-
for (const rows of [await b.syncRows([NO_NAME], false), await b.jobRows([NO_NAME], false)]) {
189+
for (const rows of [await b.syncRows([NO_NAME], false), await b.jobRows([NO_NAME], false, 'failed')]) {
186190
expect(rows[0], JSON.stringify(rows[0])).toMatchObject({ row: 1, ok: false, action: 'failed', field: 'name', code: 'required' });
187191
expect(rows[0]).not.toHaveProperty('errors');
188192
}

0 commit comments

Comments
 (0)