mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
feat(slice-05): bundle-context-packs-router module — Item 6 cross-primitive bundling part 5b (Checkpoint 7.3f step 2) (OPR.0.3.2.5)
Pure-function router module for the context_packs cross-primitive kind. Per PRD §Item 6 line 196 + grounded against context-pack-library-service consumer contract: manifest declares context_packs entries as paths to manifest.yaml files; router copies the PARENT DIRECTORY of each manifest.yaml to <targetContextPacksDir>/<basename(parentDir)>/ (the operator-host canonical layout the live consumer scans per context-pack- types.ts:9-10). Mirrors the bundle-plugins-router pattern (dir-based with copyDir FsOps) plus the workflow_specs-router degenerate-input handling (banked discipline16ebb8af). Pre-banked the consumer-invisibility + collision classes into the FIRST commit this time (avoiding the 7.3e-step3 multi-cycle catch sequence): bundle-context-packs-router.ts (+208): - ContextPacksRouterFsOps interface (exists, isDirectory, mkdirp, copyDir) - 6 status enum values: routed / missing / unsafe / not_manifest / not_directory / conflict - routeContextPacks pure function with: * Consumer-visibility prefilter: rejects declared paths whose basename is not "manifest.yaml" (status=not_manifest; consumer scans pack dirs for a top-level manifest.yaml — any other basename is invisible) * Source-side path-containment check * Pack source dir existence + isDirectory checks * Parent-dir-basename collision detection (routedDirNames Set) — first wins, second flagged status=conflict (mirror of workflow_specs basename-collision lesson on16ebb8af) * Target-side path-containment check (structurally safe via basename; kept defensive) * copyDir(sourcePackDir, targetAbs) — dir-based routing bundle-context-packs-router.test.ts (+207): 9 tests C1-C9 covering the full envelope: - C1 empty list - C2 single pack routes end-to-end - C3 multiple distinct packs route correctly - C4 missing source dir → status=missing - C5 source-side escape → status=unsafe - C6 (degenerate-input): non-manifest.yaml basename → status=not_manifest (covers both .yaml-but-wrong-name and .txt extension) - C7 (degenerate-input): parent-dir basename collision → 1st routed, 2nd conflict; ONLY 1 copyDir fires - C8 source parent is file not directory → status=not_directory - C9 mixed list aggregating all 6 status classes Discriminator-flips BOTH PROACTIVELY: Flip A — remove the not-manifest.yaml prefilter. Result: 2/9 fail (C6 + C9 mixed-list aggregate assertion). Proves prefilter specifically catches the consumer-invisibility class. Restored. Flip B — remove the routedDirNames collision detection. Result: 2/9 fail (C7 + C9 mixed-list aggregate assertion). Proves collision detection specifically catches the false-positive routedCount class. Restored. Final: 34/34 across all 4 router modules: - bundle-skills-router 8 - bundle-plugins-router 9 - bundle-workflow-specs-router 8 - bundle-context-packs-router 9 (NEW) Per carry-forward from329e2e22guard CLEAR: manifest.yaml basename check + parent-dir collision detection both ENFORCED at router level ahead of /install integration. Step 3 will add the live-consumer proof: invoke ContextPackLibraryService.scan() against the routed target dir + assert the pack is visible with expected name+version (mirror of workflow_specs scanWorkflowSpecFolder reachability proofd81456dc). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
329e2e228d
commit
a0e7e0e196
2 files changed
+411
No files matched your search
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Bundle context_packs router (Item 6 / slice-05 Checkpoint 7.3f step 2).
|
||||
*
|
||||
* Pure function. Copies context-pack directories declared in a bundle
|
||||
* manifest's context_packs[] block from the bundle's extracted tree to
|
||||
* the operator context-packs library. No daemon dependencies — fully
|
||||
* unit-testable via FsOps injection.
|
||||
*
|
||||
* Per PRD §Item 6 line 196: context_packs entries are paths to a
|
||||
* context-pack's manifest.yaml file inside the bundle. The PACK is the
|
||||
* PARENT DIRECTORY of that manifest.yaml (per context-pack-types.ts:9-10
|
||||
* and context-pack-library-service.ts:50-77: a pack is a directory whose
|
||||
* immediate children include manifest.yaml + the referenced content
|
||||
* files). The router copies that parent dir to
|
||||
* <targetContextPacksDir>/<basename(parentDir)>/ — the operator-host
|
||||
* canonical layout the live consumer scans.
|
||||
*
|
||||
* Banked degenerate-input dogfood discipline (extended from the
|
||||
* workflow_specs basename-collision + non-YAML cycles): every router
|
||||
* commit explicitly probes the failure-class edges through the live
|
||||
* consumer's contract before claiming routedCount truthful. For
|
||||
* context_packs the consumer-invisibility classes are:
|
||||
* - Declared path basename is NOT "manifest.yaml" → consumer never
|
||||
* scans (it looks for manifest.yaml at the pack dir root). status=
|
||||
* not_manifest.
|
||||
* - Two declared paths whose parent-dir basename collide → second
|
||||
* would silently overwrite the first; status=conflict; first wins.
|
||||
* - Source pack dir not present in bundle → status=missing (honest skip).
|
||||
* - Source path escapes bundleRoot → status=unsafe.
|
||||
*
|
||||
* Both-sides containment (banked
|
||||
* feedback_pre_existing_trust_boundary_reuse_canonical_helper addendum):
|
||||
* source-side under bundleRoot, target-side under targetContextPacksDir
|
||||
* (target via basename so structurally safe; defensive check retained).
|
||||
*
|
||||
* /install handler integration lands at Checkpoint 7.3f step 3 with a
|
||||
* real consumer-scan() proof against the routed dir (mirror of the
|
||||
* workflow_specs scanWorkflowSpecFolder reachability proof at d81456dc).
|
||||
*/
|
||||
|
||||
import nodePath from "node:path";
|
||||
|
||||
/** Filesystem injection point — real impl wraps node:fs. Tests substitute in-memory. */
|
||||
export interface ContextPacksRouterFsOps {
|
||||
exists: (path: string) => boolean;
|
||||
isDirectory: (path: string) => boolean;
|
||||
mkdirp: (path: string) => void;
|
||||
copyDir: (src: string, dest: string) => void;
|
||||
}
|
||||
|
||||
/** Inputs to routeContextPacks. */
|
||||
export interface RouteContextPacksInput {
|
||||
/** Absolute path to the bundle's extracted root (tmp dir from unpack). */
|
||||
bundleRoot: string;
|
||||
/** Relative paths to context-pack manifest.yaml files declared in the
|
||||
* bundle manifest's context_packs[] block. */
|
||||
declaredContextPacks: string[];
|
||||
/** Absolute path to the operator context-packs library (per
|
||||
* context-pack-types.ts:9-10: typically `<openrigHome>/context-packs`
|
||||
* or a workspace-local `.openrig/context-packs`). Caller resolves. */
|
||||
targetContextPacksDir: string;
|
||||
}
|
||||
|
||||
/** One routed context_pack (or one rejection). */
|
||||
export interface RoutedContextPackRecord {
|
||||
/** Declared path from manifest.context_packs[]. */
|
||||
declaredPath: string;
|
||||
/** "routed" = pack dir copied successfully and consumer-visible.
|
||||
* "missing" = pack manifest source not in bundle (honest skip).
|
||||
* "unsafe" = source escapes bundleRoot OR target escapes
|
||||
* targetContextPacksDir (structurally impossible given basename, kept
|
||||
* defensive).
|
||||
* "not_manifest" = declared path basename is not "manifest.yaml" —
|
||||
* consumer (context-pack-library-service.scan) scans pack dirs for a
|
||||
* top-level manifest.yaml file; any other basename is invisible-by-
|
||||
* construction.
|
||||
* "not_directory" = parent of the declared manifest path is not a
|
||||
* directory in the bundle tree.
|
||||
* "conflict" = parent-dir basename collides with an earlier routed
|
||||
* pack; first wins, second flagged so routedCount stays truthful at
|
||||
* the consumer-visible boundary (banked 16ebb8af lesson). */
|
||||
status: "routed" | "missing" | "unsafe" | "not_manifest" | "not_directory" | "conflict";
|
||||
/** Where the pack landed in the target library (absolute pack dir
|
||||
* path), if routed. */
|
||||
installedAt?: string;
|
||||
/** Human-readable detail (3-part error shape input for caller). */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/** Aggregate routing result. */
|
||||
export interface RouteContextPacksResult {
|
||||
records: RoutedContextPackRecord[];
|
||||
routedCount: number;
|
||||
rejectedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route each declared context_pack from the bundle tree to the operator
|
||||
* context-packs library. Per-entry safety: SOURCE containment under
|
||||
* bundleRoot + TARGET containment under targetContextPacksDir.
|
||||
* Degenerate inputs the consumer ignores (non-manifest.yaml basename;
|
||||
* parent-dir basename collisions) are caught BEFORE write so the
|
||||
* routedCount stays truthful at the consumer-visible boundary. Caller
|
||||
* writes the install audit record using the records returned here.
|
||||
*/
|
||||
export function routeContextPacks(
|
||||
input: RouteContextPacksInput,
|
||||
fs: ContextPacksRouterFsOps,
|
||||
): RouteContextPacksResult {
|
||||
const records: RoutedContextPackRecord[] = [];
|
||||
const bundleRootResolved = nodePath.resolve(input.bundleRoot);
|
||||
const targetRootResolved = nodePath.resolve(input.targetContextPacksDir);
|
||||
fs.mkdirp(input.targetContextPacksDir);
|
||||
// Track parent-dir basenames already routed so collisions surface as
|
||||
// conflict records, not silent overwrites (banked workflow_specs lesson
|
||||
// 16ebb8af).
|
||||
const routedDirNames = new Set<string>();
|
||||
|
||||
for (const declared of input.declaredContextPacks) {
|
||||
// Consumer-visibility prefilter: context-pack-library-service scans
|
||||
// pack dirs whose immediate child is manifest.yaml. Any declared path
|
||||
// whose basename isn't "manifest.yaml" routes to a dir the consumer
|
||||
// will never recognize as a pack. Reject pre-write so routedCount
|
||||
// stays truthful at the consumer-visible boundary.
|
||||
if (nodePath.basename(declared) !== "manifest.yaml") {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "not_manifest",
|
||||
detail: `context_pack '${declared}' basename is not 'manifest.yaml'; context-pack-library scans pack dirs for a top-level manifest.yaml`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const sourceAbs = nodePath.resolve(input.bundleRoot, declared);
|
||||
// Defense-in-depth path-containment on SOURCE (mirrors skills/
|
||||
// workflow_specs routers).
|
||||
if (sourceAbs !== bundleRootResolved && !sourceAbs.startsWith(bundleRootResolved + nodePath.sep)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "unsafe",
|
||||
detail: `context_pack path '${declared}' escapes bundle workspace; rejected`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const sourcePackDir = nodePath.dirname(sourceAbs);
|
||||
if (!fs.exists(sourcePackDir)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "missing",
|
||||
detail: `context_pack source dir '${nodePath.dirname(declared)}' not present in bundle; skipped`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!fs.isDirectory(sourcePackDir)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "not_directory",
|
||||
detail: `context_pack parent of '${declared}' is not a directory; skipped`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const dirName = nodePath.basename(sourcePackDir);
|
||||
// Target = <targetContextPacksDir>/<basename(parentDir)>/ (the operator-
|
||||
// host canonical layout per context-pack-types.ts:9-10).
|
||||
const targetAbs = nodePath.resolve(input.targetContextPacksDir, dirName);
|
||||
// Sanity check (basename is structurally safe but the resolve could
|
||||
// theoretically be a no-op for "" → keep the check, expected always-
|
||||
// true).
|
||||
if (targetAbs !== targetRootResolved && !targetAbs.startsWith(targetRootResolved + nodePath.sep)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "unsafe",
|
||||
detail: `context_pack target path for '${declared}' escapes target context-packs library; rejected`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (routedDirNames.has(dirName)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "conflict",
|
||||
detail: `context_pack parent-dir basename '${dirName}' collides with an earlier declared path; only the first is routed (banked collision-detection lesson)`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
fs.copyDir(sourcePackDir, targetAbs);
|
||||
routedDirNames.add(dirName);
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "routed",
|
||||
installedAt: targetAbs,
|
||||
});
|
||||
}
|
||||
|
||||
const routedCount = records.filter((r) => r.status === "routed").length;
|
||||
const rejectedCount = records.length - routedCount;
|
||||
return { records, routedCount, rejectedCount };
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { routeContextPacks, type ContextPacksRouterFsOps, type RouteContextPacksInput } from "../src/domain/bundle-context-packs-router.js";
|
||||
|
||||
// Item 6 / slice-05 Checkpoint 7.3f step 2: bundle-context-packs-router
|
||||
// pure-function tests. Mirrors plugins router (dir-based) with the
|
||||
// context-pack-specific degenerate-input handling per the consumer
|
||||
// contract (context-pack-library-service.scan walks pack dirs whose
|
||||
// immediate child is manifest.yaml).
|
||||
|
||||
function mockFs(initial: { dirs?: string[] } = {}): ContextPacksRouterFsOps & {
|
||||
_copyCalls: Array<{ src: string; dest: string }>;
|
||||
_mkdirpCalls: string[];
|
||||
_dirs: Set<string>;
|
||||
} {
|
||||
const dirs = new Set<string>(initial.dirs ?? []);
|
||||
const copyCalls: Array<{ src: string; dest: string }> = [];
|
||||
const mkdirpCalls: string[] = [];
|
||||
return {
|
||||
_copyCalls: copyCalls,
|
||||
_mkdirpCalls: mkdirpCalls,
|
||||
_dirs: dirs,
|
||||
exists: (p: string) => dirs.has(p),
|
||||
isDirectory: (p: string) => dirs.has(p),
|
||||
mkdirp: (p: string) => { mkdirpCalls.push(p); dirs.add(p); },
|
||||
copyDir: (src: string, dest: string) => { copyCalls.push({ src, dest }); dirs.add(dest); },
|
||||
};
|
||||
}
|
||||
|
||||
const BUNDLE_ROOT = "/bundle/root";
|
||||
const TARGET = "/operator/.openrig/context-packs";
|
||||
|
||||
function makeInput(overrides?: Partial<RouteContextPacksInput>): RouteContextPacksInput {
|
||||
return {
|
||||
bundleRoot: BUNDLE_ROOT,
|
||||
declaredContextPacks: [],
|
||||
targetContextPacksDir: TARGET,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("routeContextPacks", () => {
|
||||
// C1: empty list → empty records + target dir mkdirp'd
|
||||
it("empty declaredContextPacks produces empty records but still mkdirp's target", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeContextPacks(makeInput(), fs);
|
||||
expect(result.records).toEqual([]);
|
||||
expect(result.routedCount).toBe(0);
|
||||
expect(result.rejectedCount).toBe(0);
|
||||
expect(fs._mkdirpCalls).toContain(TARGET);
|
||||
});
|
||||
|
||||
// C2: routes one pack: copyDir from sourceParentDir to target/<dirname>
|
||||
it("routes one context_pack: parent dir copied to target/<dirname>", () => {
|
||||
const fs = mockFs({ dirs: [`${BUNDLE_ROOT}/context-packs/intent`] });
|
||||
const result = routeContextPacks(
|
||||
makeInput({ declaredContextPacks: ["context-packs/intent/manifest.yaml"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(1);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[0]!.installedAt).toBe(`${TARGET}/intent`);
|
||||
expect(fs._copyCalls).toHaveLength(1);
|
||||
expect(fs._copyCalls[0]).toEqual({
|
||||
src: `${BUNDLE_ROOT}/context-packs/intent`,
|
||||
dest: `${TARGET}/intent`,
|
||||
});
|
||||
});
|
||||
|
||||
// C3: multiple distinct packs route correctly
|
||||
it("routes multiple distinct context_packs each to target/<dirname>", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [`${BUNDLE_ROOT}/context-packs/intent`, `${BUNDLE_ROOT}/context-packs/persona`],
|
||||
});
|
||||
const result = routeContextPacks(
|
||||
makeInput({
|
||||
declaredContextPacks: [
|
||||
"context-packs/intent/manifest.yaml",
|
||||
"context-packs/persona/manifest.yaml",
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(2);
|
||||
expect(fs._copyCalls).toHaveLength(2);
|
||||
});
|
||||
|
||||
// C4: missing source pack dir skipped honestly
|
||||
it("missing source pack dir → status=missing (honest-scoping)", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeContextPacks(
|
||||
makeInput({ declaredContextPacks: ["context-packs/absent/manifest.yaml"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(0);
|
||||
expect(result.records[0]!.status).toBe("missing");
|
||||
expect(result.records[0]!.detail).toContain("not present");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// C5: unsafe source path escaping bundle workspace rejected
|
||||
it("unsafe source path escaping bundle workspace → status=unsafe", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeContextPacks(
|
||||
makeInput({ declaredContextPacks: ["../escape/manifest.yaml"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("unsafe");
|
||||
expect(result.records[0]!.detail).toContain("escapes bundle workspace");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// C6 (degenerate-input): basename not manifest.yaml — consumer-invisible
|
||||
// class. Per banked PRE-handoff degenerate-input dogfood discipline.
|
||||
it("declared path with non-manifest.yaml basename → status=not_manifest", () => {
|
||||
const fs = mockFs({ dirs: [`${BUNDLE_ROOT}/context-packs/oddpack`] });
|
||||
const result = routeContextPacks(
|
||||
makeInput({
|
||||
declaredContextPacks: [
|
||||
"context-packs/oddpack/pack.yaml",
|
||||
"context-packs/oddpack/manifest.txt",
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.records).toHaveLength(2);
|
||||
expect(result.routedCount).toBe(0);
|
||||
expect(result.records[0]!.status).toBe("not_manifest");
|
||||
expect(result.records[0]!.detail).toContain("manifest.yaml");
|
||||
expect(result.records[1]!.status).toBe("not_manifest");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// C7 (degenerate-input): parent-dir basename collision — first wins,
|
||||
// second flagged conflict. Banked workflow_specs B1 lesson applied.
|
||||
it("two declared packs sharing parent-dir basename → first routed, second conflict", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [
|
||||
`${BUNDLE_ROOT}/a/intent`,
|
||||
`${BUNDLE_ROOT}/b/intent`,
|
||||
],
|
||||
});
|
||||
const result = routeContextPacks(
|
||||
makeInput({
|
||||
declaredContextPacks: [
|
||||
"a/intent/manifest.yaml",
|
||||
"b/intent/manifest.yaml",
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.records).toHaveLength(2);
|
||||
expect(result.routedCount).toBe(1);
|
||||
expect(result.rejectedCount).toBe(1);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[0]!.installedAt).toBe(`${TARGET}/intent`);
|
||||
expect(result.records[1]!.status).toBe("conflict");
|
||||
expect(result.records[1]!.detail).toContain("intent");
|
||||
expect(result.records[1]!.detail).toContain("collides");
|
||||
// Only the first copyDir fires.
|
||||
expect(fs._copyCalls).toHaveLength(1);
|
||||
expect(fs._copyCalls[0]!.src).toBe(`${BUNDLE_ROOT}/a/intent`);
|
||||
});
|
||||
|
||||
// C8: pack source path exists but is a file not a directory — edge case
|
||||
// (unusual; manifest.yaml dirname resolved to a file). Skipped honestly.
|
||||
it("source parent exists but is a file (not directory) → status=not_directory", () => {
|
||||
// mockFs treats anything in dirs[] as a dir; for this case construct
|
||||
// an exists-but-not-isDirectory state by overriding isDirectory.
|
||||
const fs = mockFs();
|
||||
fs._dirs.add(`${BUNDLE_ROOT}/oddpath`); // exists
|
||||
// Override isDirectory just for this path:
|
||||
const origIsDir = fs.isDirectory;
|
||||
fs.isDirectory = (p: string) => p === `${BUNDLE_ROOT}/oddpath` ? false : origIsDir(p);
|
||||
const result = routeContextPacks(
|
||||
makeInput({ declaredContextPacks: ["oddpath/manifest.yaml"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("not_directory");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// C9: mixed list — routed + missing + unsafe + not_manifest + conflict
|
||||
// aggregate correctly
|
||||
it("mixed declared list aggregates correctly across all rejection classes", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [
|
||||
`${BUNDLE_ROOT}/context-packs/ok`,
|
||||
`${BUNDLE_ROOT}/context-packs/dup`,
|
||||
`${BUNDLE_ROOT}/elsewhere/dup`,
|
||||
],
|
||||
});
|
||||
const result = routeContextPacks(
|
||||
makeInput({
|
||||
declaredContextPacks: [
|
||||
"context-packs/ok/manifest.yaml", // routed
|
||||
"context-packs/absent/manifest.yaml", // missing
|
||||
"../escape/manifest.yaml", // unsafe
|
||||
"context-packs/odd/pack.yaml", // not_manifest
|
||||
"context-packs/dup/manifest.yaml", // routed (1st dup)
|
||||
"elsewhere/dup/manifest.yaml", // conflict (basename dup with above)
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.records).toHaveLength(6);
|
||||
expect(result.routedCount).toBe(2);
|
||||
expect(result.rejectedCount).toBe(4);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[1]!.status).toBe("missing");
|
||||
expect(result.records[2]!.status).toBe("unsafe");
|
||||
expect(result.records[3]!.status).toBe("not_manifest");
|
||||
expect(result.records[4]!.status).toBe("routed");
|
||||
expect(result.records[5]!.status).toBe("conflict");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user