@lovrozagar/oat 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2006
- package/dist/cli.js +283 -59
- package/dist/config/define-config.d.ts +8 -0
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/config/define-config.js.map +1 -1
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +86 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/conformance/effects.d.ts +9 -0
- package/dist/conformance/effects.d.ts.map +1 -0
- package/dist/conformance/effects.js +276 -0
- package/dist/conformance/effects.js.map +1 -0
- package/dist/conformance/expected-failures.d.ts +18 -0
- package/dist/conformance/expected-failures.d.ts.map +1 -0
- package/dist/conformance/expected-failures.js +48 -0
- package/dist/conformance/expected-failures.js.map +1 -0
- package/dist/conformance/feature-gate.d.ts +7 -0
- package/dist/conformance/feature-gate.d.ts.map +1 -0
- package/dist/conformance/feature-gate.js +428 -0
- package/dist/conformance/feature-gate.js.map +1 -0
- package/dist/conformance/fuzz.d.ts +108 -0
- package/dist/conformance/fuzz.d.ts.map +1 -0
- package/dist/conformance/fuzz.js +246 -0
- package/dist/conformance/fuzz.js.map +1 -0
- package/dist/conformance/leaks.d.ts +10 -0
- package/dist/conformance/leaks.d.ts.map +1 -0
- package/dist/conformance/leaks.js +23 -0
- package/dist/conformance/leaks.js.map +1 -0
- package/dist/conformance/pool.d.ts +49 -0
- package/dist/conformance/pool.d.ts.map +1 -0
- package/dist/conformance/pool.js +75 -0
- package/dist/conformance/pool.js.map +1 -0
- package/dist/conformance/rate-limit.d.ts +7 -0
- package/dist/conformance/rate-limit.d.ts.map +1 -0
- package/dist/conformance/rate-limit.js +372 -0
- package/dist/conformance/rate-limit.js.map +1 -0
- package/dist/conformance/scope.d.ts +15 -0
- package/dist/conformance/scope.d.ts.map +1 -0
- package/dist/conformance/scope.js +78 -0
- package/dist/conformance/scope.js.map +1 -0
- package/dist/conformance/seed-contract.d.ts +11 -0
- package/dist/conformance/seed-contract.d.ts.map +1 -0
- package/dist/conformance/seed-contract.js +680 -0
- package/dist/conformance/seed-contract.js.map +1 -0
- package/dist/conformance/shapes.d.ts +35 -0
- package/dist/conformance/shapes.d.ts.map +1 -0
- package/dist/conformance/shapes.js +162 -0
- package/dist/conformance/shapes.js.map +1 -0
- package/dist/conformance/specs.d.ts +21 -0
- package/dist/conformance/specs.d.ts.map +1 -0
- package/dist/conformance/specs.js +306 -0
- package/dist/conformance/specs.js.map +1 -0
- package/dist/conformance/suite.d.ts +209 -0
- package/dist/conformance/suite.d.ts.map +1 -0
- package/dist/conformance/suite.js +1538 -0
- package/dist/conformance/suite.js.map +1 -0
- package/dist/conformance/unique.d.ts +7 -0
- package/dist/conformance/unique.d.ts.map +1 -0
- package/dist/conformance/unique.js +586 -0
- package/dist/conformance/unique.js.map +1 -0
- package/dist/conformance/worker.d.ts +3 -0
- package/dist/conformance/worker.d.ts.map +1 -0
- package/dist/conformance/worker.js +27 -0
- package/dist/conformance/worker.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/reference/defects.d.ts +71 -1
- package/dist/reference/defects.d.ts.map +1 -1
- package/dist/reference/defects.js +74 -8
- package/dist/reference/defects.js.map +1 -1
- package/dist/reference/dialect.d.ts +6 -4
- package/dist/reference/dialect.d.ts.map +1 -1
- package/dist/reference/dialect.js +1 -0
- package/dist/reference/dialect.js.map +1 -1
- package/dist/reference/http.d.ts +17 -25
- package/dist/reference/http.d.ts.map +1 -1
- package/dist/reference/http.js +574 -224
- package/dist/reference/http.js.map +1 -1
- package/dist/reference/listing.d.ts +51 -0
- package/dist/reference/listing.d.ts.map +1 -0
- package/dist/reference/listing.js +256 -0
- package/dist/reference/listing.js.map +1 -0
- package/dist/reference/model.d.ts +35 -1
- package/dist/reference/model.d.ts.map +1 -1
- package/dist/reference/model.js +42 -0
- package/dist/reference/model.js.map +1 -1
- package/dist/reference/query.d.ts +132 -43
- package/dist/reference/query.d.ts.map +1 -1
- package/dist/reference/query.js +397 -315
- package/dist/reference/query.js.map +1 -1
- package/dist/reference/shapes.d.ts +60 -0
- package/dist/reference/shapes.d.ts.map +1 -0
- package/dist/reference/shapes.js +142 -0
- package/dist/reference/shapes.js.map +1 -0
- package/dist/reference/spec.d.ts +9 -2
- package/dist/reference/spec.d.ts.map +1 -1
- package/dist/reference/spec.js +291 -71
- package/dist/reference/spec.js.map +1 -1
- package/dist/reference/store-api.d.ts +14 -43
- package/dist/reference/store-api.d.ts.map +1 -1
- package/dist/reference/store-api.js +8 -58
- package/dist/reference/store-api.js.map +1 -1
- package/dist/reference/stores/memory.d.ts +12 -15
- package/dist/reference/stores/memory.d.ts.map +1 -1
- package/dist/reference/stores/memory.js +22 -70
- package/dist/reference/stores/memory.js.map +1 -1
- package/dist/reference/stores/postgres.d.ts +11 -23
- package/dist/reference/stores/postgres.d.ts.map +1 -1
- package/dist/reference/stores/postgres.js +53 -334
- package/dist/reference/stores/postgres.js.map +1 -1
- package/dist/reference/stores/sql-compile.d.ts +36 -0
- package/dist/reference/stores/sql-compile.d.ts.map +1 -0
- package/dist/reference/stores/sql-compile.js +95 -0
- package/dist/reference/stores/sql-compile.js.map +1 -0
- package/dist/reference/stores/sqlite.d.ts +12 -27
- package/dist/reference/stores/sqlite.d.ts.map +1 -1
- package/dist/reference/stores/sqlite.js +54 -355
- package/dist/reference/stores/sqlite.js.map +1 -1
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +23 -9
- package/dist/report/console.js.map +1 -1
- package/dist/report/matrix.d.ts +2 -0
- package/dist/report/matrix.d.ts.map +1 -1
- package/dist/report/matrix.js +63 -10
- package/dist/report/matrix.js.map +1 -1
- package/dist/report/render.d.ts +10 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +75 -13
- package/dist/report/render.js.map +1 -1
- package/dist/runtime/async.d.ts +16 -3
- package/dist/runtime/async.d.ts.map +1 -1
- package/dist/runtime/async.js +24 -19
- package/dist/runtime/async.js.map +1 -1
- package/dist/runtime/auth.d.ts +8 -9
- package/dist/runtime/auth.d.ts.map +1 -1
- package/dist/runtime/auth.js +60 -62
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/body.d.ts +15 -3
- package/dist/runtime/body.d.ts.map +1 -1
- package/dist/runtime/body.js +61 -12
- package/dist/runtime/body.js.map +1 -1
- package/dist/runtime/check-costs.d.ts +15 -0
- package/dist/runtime/check-costs.d.ts.map +1 -0
- package/dist/runtime/check-costs.js +114 -0
- package/dist/runtime/check-costs.js.map +1 -0
- package/dist/runtime/check-ids.d.ts +15 -0
- package/dist/runtime/check-ids.d.ts.map +1 -0
- package/dist/runtime/check-ids.js +140 -0
- package/dist/runtime/check-ids.js.map +1 -0
- package/dist/runtime/checks.d.ts +88 -20
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +3288 -1947
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/client.d.ts +112 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +271 -39
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/cookies.d.ts +16 -2
- package/dist/runtime/cookies.d.ts.map +1 -1
- package/dist/runtime/cookies.js +83 -22
- package/dist/runtime/cookies.js.map +1 -1
- package/dist/runtime/exchanges.d.ts +4 -5
- package/dist/runtime/exchanges.d.ts.map +1 -1
- package/dist/runtime/exchanges.js +33 -55
- package/dist/runtime/exchanges.js.map +1 -1
- package/dist/runtime/exit.d.ts +40 -0
- package/dist/runtime/exit.d.ts.map +1 -0
- package/dist/runtime/exit.js +48 -0
- package/dist/runtime/exit.js.map +1 -0
- package/dist/runtime/feature-gate.d.ts +0 -9
- package/dist/runtime/feature-gate.d.ts.map +1 -1
- package/dist/runtime/feature-gate.js +0 -21
- package/dist/runtime/feature-gate.js.map +1 -1
- package/dist/runtime/finding.d.ts +49 -8
- package/dist/runtime/finding.d.ts.map +1 -1
- package/dist/runtime/finding.js +51 -8
- package/dist/runtime/finding.js.map +1 -1
- package/dist/runtime/fixture.d.ts +35 -28
- package/dist/runtime/fixture.d.ts.map +1 -1
- package/dist/runtime/fixture.js +92 -731
- package/dist/runtime/fixture.js.map +1 -1
- package/dist/runtime/generate.d.ts +63 -0
- package/dist/runtime/generate.d.ts.map +1 -0
- package/dist/runtime/generate.js +635 -0
- package/dist/runtime/generate.js.map +1 -0
- package/dist/runtime/headers.d.ts +6 -0
- package/dist/runtime/headers.d.ts.map +1 -0
- package/dist/runtime/headers.js +16 -0
- package/dist/runtime/headers.js.map +1 -0
- package/dist/runtime/path.d.ts +7 -0
- package/dist/runtime/path.d.ts.map +1 -0
- package/dist/runtime/path.js +44 -0
- package/dist/runtime/path.js.map +1 -0
- package/dist/runtime/payloads.d.ts +2 -0
- package/dist/runtime/payloads.d.ts.map +1 -1
- package/dist/runtime/payloads.js +2 -1
- package/dist/runtime/payloads.js.map +1 -1
- package/dist/runtime/rate-limit.d.ts +6 -4
- package/dist/runtime/rate-limit.d.ts.map +1 -1
- package/dist/runtime/rate-limit.js +22 -33
- package/dist/runtime/rate-limit.js.map +1 -1
- package/dist/runtime/redact.d.ts +24 -0
- package/dist/runtime/redact.d.ts.map +1 -0
- package/dist/runtime/redact.js +98 -0
- package/dist/runtime/redact.js.map +1 -0
- package/dist/runtime/run.d.ts +34 -2
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +374 -138
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runs.d.ts +5 -0
- package/dist/runtime/runs.d.ts.map +1 -1
- package/dist/runtime/runs.js +7 -1
- package/dist/runtime/runs.js.map +1 -1
- package/dist/runtime/teardown.d.ts +17 -4
- package/dist/runtime/teardown.d.ts.map +1 -1
- package/dist/runtime/teardown.js +49 -16
- package/dist/runtime/teardown.js.map +1 -1
- package/dist/runtime/transcript.d.ts +34 -2
- package/dist/runtime/transcript.d.ts.map +1 -1
- package/dist/runtime/transcript.js +119 -24
- package/dist/runtime/transcript.js.map +1 -1
- package/dist/runtime/unique.d.ts +2 -11
- package/dist/runtime/unique.d.ts.map +1 -1
- package/dist/runtime/unique.js +27 -53
- package/dist/runtime/unique.js.map +1 -1
- package/dist/runtime/validate.d.ts +13 -2
- package/dist/runtime/validate.d.ts.map +1 -1
- package/dist/runtime/validate.js +51 -31
- package/dist/runtime/validate.js.map +1 -1
- package/dist/runtime/wait.d.ts +2 -1
- package/dist/runtime/wait.d.ts.map +1 -1
- package/dist/runtime/wait.js +2 -36
- package/dist/runtime/wait.js.map +1 -1
- package/dist/runtime/world.d.ts +24 -1
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +101 -26
- package/dist/runtime/world.js.map +1 -1
- package/dist/spec/collection.d.ts +8 -2
- package/dist/spec/collection.d.ts.map +1 -1
- package/dist/spec/collection.js +47 -7
- package/dist/spec/collection.js.map +1 -1
- package/dist/spec/extensions.d.ts +11 -1
- package/dist/spec/extensions.d.ts.map +1 -1
- package/dist/spec/extensions.js +10 -2
- package/dist/spec/extensions.js.map +1 -1
- package/dist/spec/graph.d.ts +46 -0
- package/dist/spec/graph.d.ts.map +1 -1
- package/dist/spec/graph.js +175 -11
- package/dist/spec/graph.js.map +1 -1
- package/dist/spec/load.d.ts +25 -5
- package/dist/spec/load.d.ts.map +1 -1
- package/dist/spec/load.js +160 -59
- package/dist/spec/load.js.map +1 -1
- package/dist/spec/query-capabilities.d.ts +4 -0
- package/dist/spec/query-capabilities.d.ts.map +1 -1
- package/dist/spec/query-capabilities.js +6 -0
- package/dist/spec/query-capabilities.js.map +1 -1
- package/dist/spec/schema.d.ts +20 -0
- package/dist/spec/schema.d.ts.map +1 -0
- package/dist/spec/schema.js +118 -0
- package/dist/spec/schema.js.map +1 -0
- package/dist/spec/tag-schema.d.ts +12 -0
- package/dist/spec/tag-schema.d.ts.map +1 -0
- package/dist/spec/tag-schema.js +144 -0
- package/dist/spec/tag-schema.js.map +1 -0
- package/dist/spec/types.js +17 -1
- package/dist/spec/types.js.map +1 -1
- package/docs/api.md +59 -0
- package/docs/assumptions.md +99 -0
- package/docs/checks.md +183 -0
- package/docs/commands.md +214 -0
- package/docs/configuration.md +650 -0
- package/docs/model.md +184 -0
- package/docs/reference-defects.md +94 -0
- package/docs/reports.md +211 -0
- package/docs/tags.md +403 -0
- package/package.json +3 -1
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x-unique` is a declared fact. These cases prove a 409 duplicate is a pass, a 2xx is
|
|
3
|
+
* BACKEND_BUG, and that missing / malformed tags do not invent a unique check.
|
|
4
|
+
*/
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { report } from "../report/console.js";
|
|
7
|
+
import { run } from "../runtime/run.js";
|
|
8
|
+
import { formatUniqueSets, readUnique } from "../spec/extensions.js";
|
|
9
|
+
import { buildModel } from "../spec/graph.js";
|
|
10
|
+
import { dereference } from "../spec/load.js";
|
|
11
|
+
const ITEM = {
|
|
12
|
+
properties: { email: { type: "string" }, id: { type: "string" }, name: { type: "string" }, slug: { type: "string" } },
|
|
13
|
+
required: ["id", "name"],
|
|
14
|
+
type: "object",
|
|
15
|
+
};
|
|
16
|
+
const ERROR_SCHEMA = {
|
|
17
|
+
properties: { error_key: { type: "string" }, status: { type: "integer" } },
|
|
18
|
+
required: ["error_key"],
|
|
19
|
+
type: "object",
|
|
20
|
+
};
|
|
21
|
+
const GATE_SCHEMA = {
|
|
22
|
+
properties: {
|
|
23
|
+
error_key: { type: "string" },
|
|
24
|
+
vars: { additionalProperties: true, type: "object" },
|
|
25
|
+
},
|
|
26
|
+
type: "object",
|
|
27
|
+
};
|
|
28
|
+
function widgetSpec(options = {}) {
|
|
29
|
+
const uniqueExt = options.omitTag === true ? {} : { "x-unique": options.tag === undefined ? [["name"]] : options.tag };
|
|
30
|
+
const createBodyProps = {
|
|
31
|
+
email: { type: "string" },
|
|
32
|
+
name: { type: "string", maxLength: 128 },
|
|
33
|
+
slug: { type: "string" },
|
|
34
|
+
};
|
|
35
|
+
const updateProps = {};
|
|
36
|
+
for (const name of options.updateBody ?? ["email", "name", "slug"]) {
|
|
37
|
+
updateProps[name] = createBodyProps[name] ?? { type: "string" };
|
|
38
|
+
}
|
|
39
|
+
if ((options.updateBody ?? []).includes("workspace_id") || options.immutable?.includes("workspace_id")) {
|
|
40
|
+
updateProps.workspace_id = { type: "string" };
|
|
41
|
+
}
|
|
42
|
+
const create = {
|
|
43
|
+
operationId: "widget.create",
|
|
44
|
+
parameters: [
|
|
45
|
+
{ in: "path", name: "org_id", required: true, schema: { type: "string" } },
|
|
46
|
+
{
|
|
47
|
+
in: "header",
|
|
48
|
+
name: "Idempotency-Key",
|
|
49
|
+
required: options.idempotencyRequired === true,
|
|
50
|
+
schema: { type: "string" },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
requestBody: {
|
|
54
|
+
content: {
|
|
55
|
+
"application/json": {
|
|
56
|
+
schema: {
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
properties: createBodyProps,
|
|
59
|
+
required: ["name"],
|
|
60
|
+
type: "object",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: true,
|
|
65
|
+
},
|
|
66
|
+
responses: {
|
|
67
|
+
"201": { content: { "application/json": { schema: ITEM } }, description: "created" },
|
|
68
|
+
"402": { description: "plan limit" },
|
|
69
|
+
"403": { content: { "application/json": { schema: GATE_SCHEMA } }, description: "forbidden" },
|
|
70
|
+
"409": {
|
|
71
|
+
content: { "application/json": { schema: options.create409Schema ?? ERROR_SCHEMA } },
|
|
72
|
+
description: "conflict",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"x-entity": { action: "create", identity: "id", name: "widget" },
|
|
76
|
+
"x-generated": options.generated ?? ["id"],
|
|
77
|
+
...uniqueExt,
|
|
78
|
+
...(options.featureGate === undefined ? {} : { "x-feature-gate": options.featureGate }),
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
info: { title: "unique harness", version: "1" },
|
|
82
|
+
openapi: "3.1.0",
|
|
83
|
+
paths: {
|
|
84
|
+
"/v1/orgs/{org_id}/widgets": {
|
|
85
|
+
get: {
|
|
86
|
+
operationId: "widget.list",
|
|
87
|
+
parameters: [{ in: "path", name: "org_id", required: true, schema: { type: "string" } }],
|
|
88
|
+
responses: {
|
|
89
|
+
"200": {
|
|
90
|
+
content: {
|
|
91
|
+
"application/json": {
|
|
92
|
+
schema: {
|
|
93
|
+
properties: { widgets: { items: ITEM, type: "array" } },
|
|
94
|
+
required: ["widgets"],
|
|
95
|
+
type: "object",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
description: "ok",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"x-entity": { action: "list", identity: "id", name: "widget" },
|
|
103
|
+
},
|
|
104
|
+
parameters: [{ in: "path", name: "org_id", required: true, schema: { type: "string" } }],
|
|
105
|
+
post: create,
|
|
106
|
+
},
|
|
107
|
+
"/v1/orgs/{org_id}/widgets/{widget_id}": {
|
|
108
|
+
delete: {
|
|
109
|
+
operationId: "widget.delete",
|
|
110
|
+
parameters: [
|
|
111
|
+
{ in: "path", name: "org_id", required: true, schema: { type: "string" } },
|
|
112
|
+
{ in: "path", name: "widget_id", required: true, schema: { type: "string" } },
|
|
113
|
+
],
|
|
114
|
+
responses: { "204": { description: "gone" } },
|
|
115
|
+
"x-entity": { action: "delete", identity: "id", name: "widget" },
|
|
116
|
+
},
|
|
117
|
+
get: {
|
|
118
|
+
operationId: "widget.read",
|
|
119
|
+
parameters: [
|
|
120
|
+
{ in: "path", name: "org_id", required: true, schema: { type: "string" } },
|
|
121
|
+
{ in: "path", name: "widget_id", required: true, schema: { type: "string" } },
|
|
122
|
+
],
|
|
123
|
+
responses: {
|
|
124
|
+
"200": { content: { "application/json": { schema: ITEM } }, description: "ok" },
|
|
125
|
+
"404": { content: { "application/json": { schema: ERROR_SCHEMA } }, description: "missing" },
|
|
126
|
+
},
|
|
127
|
+
"x-entity": { action: "read", identity: "id", name: "widget" },
|
|
128
|
+
},
|
|
129
|
+
patch: {
|
|
130
|
+
operationId: "widget.update",
|
|
131
|
+
parameters: [
|
|
132
|
+
{ in: "path", name: "org_id", required: true, schema: { type: "string" } },
|
|
133
|
+
{ in: "path", name: "widget_id", required: true, schema: { type: "string" } },
|
|
134
|
+
],
|
|
135
|
+
requestBody: {
|
|
136
|
+
content: {
|
|
137
|
+
"application/json": {
|
|
138
|
+
schema: { additionalProperties: false, properties: updateProps, type: "object" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
responses: {
|
|
143
|
+
"200": { content: { "application/json": { schema: ITEM } }, description: "ok" },
|
|
144
|
+
"402": { description: "plan limit" },
|
|
145
|
+
"403": { content: { "application/json": { schema: GATE_SCHEMA } }, description: "forbidden" },
|
|
146
|
+
"409": { content: { "application/json": { schema: ERROR_SCHEMA } }, description: "conflict" },
|
|
147
|
+
},
|
|
148
|
+
"x-entity": { action: "update", identity: "id", name: "widget" },
|
|
149
|
+
"x-immutable": options.immutable ?? [],
|
|
150
|
+
...uniqueExt,
|
|
151
|
+
...(options.featureGate === undefined ? {} : { "x-feature-gate": options.featureGate }),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function send(res, status, body) {
|
|
158
|
+
if (body === undefined) {
|
|
159
|
+
res.writeHead(status);
|
|
160
|
+
res.end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const text = JSON.stringify(body);
|
|
164
|
+
res.writeHead(status, { "content-length": String(Buffer.byteLength(text)), "content-type": "application/json" });
|
|
165
|
+
res.end(text);
|
|
166
|
+
}
|
|
167
|
+
function readJson(req) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const chunks = [];
|
|
170
|
+
req.on("data", (chunk) => {
|
|
171
|
+
chunks.push(chunk);
|
|
172
|
+
});
|
|
173
|
+
req.on("end", () => {
|
|
174
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
175
|
+
if (text === "") {
|
|
176
|
+
resolve(undefined);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
resolve(JSON.parse(text));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
reject(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
req.on("error", reject);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function conflict(sets, existing, incoming, exceptId) {
|
|
190
|
+
for (const set of sets) {
|
|
191
|
+
const hit = existing.find((row) => {
|
|
192
|
+
if (exceptId !== undefined && row.id === exceptId)
|
|
193
|
+
return false;
|
|
194
|
+
return set.every((col) => incoming[col] !== undefined && String(row[col]) === String(incoming[col]));
|
|
195
|
+
});
|
|
196
|
+
if (hit !== undefined)
|
|
197
|
+
return set;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function setsOf(tag) {
|
|
202
|
+
const parsed = readUnique({ "x-unique": tag });
|
|
203
|
+
return parsed ?? [];
|
|
204
|
+
}
|
|
205
|
+
async function serveHarness(options) {
|
|
206
|
+
const spec = widgetSpec(options.spec);
|
|
207
|
+
const tag = options.spec.omitTag === true ? null : (options.spec.tag ?? [["name"]]);
|
|
208
|
+
const uniqueSets = tag === null ? [] : setsOf(tag);
|
|
209
|
+
const rows = new Map();
|
|
210
|
+
const posts = [];
|
|
211
|
+
let seq = 0;
|
|
212
|
+
if (options.listSeeded === true) {
|
|
213
|
+
rows.set("w_existing", { email: "seed@example.test", id: "w_existing", name: "Existing Widget", slug: "existing" });
|
|
214
|
+
}
|
|
215
|
+
const server = createServer((req, res) => {
|
|
216
|
+
void (async () => {
|
|
217
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
218
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
219
|
+
if (url.pathname === "/v1/openapi/spec" && method === "GET")
|
|
220
|
+
return send(res, 200, spec);
|
|
221
|
+
const header = req.headers.authorization;
|
|
222
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
223
|
+
return send(res, 401, { error_key: "unauthorized" });
|
|
224
|
+
}
|
|
225
|
+
if (url.pathname === "/v1/orgs/org_1/widgets" && method === "GET") {
|
|
226
|
+
return send(res, 200, { widgets: [...rows.values()] });
|
|
227
|
+
}
|
|
228
|
+
if (url.pathname === "/v1/orgs/org_1/widgets" && method === "POST") {
|
|
229
|
+
const body = ((await readJson(req)) ?? {});
|
|
230
|
+
posts.push({ body, headers: { ...req.headers } });
|
|
231
|
+
if (options.seedConflict === true && posts.length === 1) {
|
|
232
|
+
return send(res, 409, { error_key: "conflict", status: 409 });
|
|
233
|
+
}
|
|
234
|
+
const colliding = conflict(uniqueSets, [...rows.values()], body);
|
|
235
|
+
if (colliding !== null) {
|
|
236
|
+
const mode = options.duplicate ?? "409";
|
|
237
|
+
if (mode === "gate-403") {
|
|
238
|
+
return send(res, 403, {
|
|
239
|
+
error_key: "forbidden",
|
|
240
|
+
vars: { feature: options.spec.featureGate ?? "widgets", type: "feature_gate" },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (mode === "402")
|
|
244
|
+
return send(res, 402, { error_key: "payment_required" });
|
|
245
|
+
if (mode === "409-drift")
|
|
246
|
+
return send(res, 409, { oops: true });
|
|
247
|
+
if (mode === "201") {
|
|
248
|
+
/* fall through and insert */
|
|
249
|
+
}
|
|
250
|
+
else if (mode === "per-set") {
|
|
251
|
+
if (colliding.includes("name") || colliding.includes("email")) {
|
|
252
|
+
return send(res, 409, { error_key: "conflict", status: 409 });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
return send(res, 409, { error_key: "conflict", status: 409 });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const id = `w_${String((seq += 1))}`;
|
|
260
|
+
const row = {
|
|
261
|
+
email: typeof body.email === "string" ? body.email : `${id}@example.test`,
|
|
262
|
+
id,
|
|
263
|
+
name: typeof body.name === "string" ? body.name : id,
|
|
264
|
+
slug: typeof body.slug === "string" ? body.slug : id,
|
|
265
|
+
};
|
|
266
|
+
rows.set(id, row);
|
|
267
|
+
return send(res, 201, row);
|
|
268
|
+
}
|
|
269
|
+
const item = /^\/v1\/orgs\/org_1\/widgets\/([^/]+)$/.exec(url.pathname);
|
|
270
|
+
if (item !== null && method === "GET") {
|
|
271
|
+
const row = rows.get(decodeURIComponent(item[1] ?? ""));
|
|
272
|
+
return row === undefined ? send(res, 404, { error_key: "not_found" }) : send(res, 200, row);
|
|
273
|
+
}
|
|
274
|
+
if (item !== null && method === "PATCH") {
|
|
275
|
+
const id = decodeURIComponent(item[1] ?? "");
|
|
276
|
+
const row = rows.get(id);
|
|
277
|
+
if (row === undefined)
|
|
278
|
+
return send(res, 404, { error_key: "not_found" });
|
|
279
|
+
const body = ((await readJson(req)) ?? {});
|
|
280
|
+
posts.push({ body, headers: { ...req.headers } });
|
|
281
|
+
const next = { ...row, ...body };
|
|
282
|
+
const colliding = conflict(uniqueSets, [...rows.values()], next, id);
|
|
283
|
+
const mode = options.updateDuplicate ?? "409";
|
|
284
|
+
if (colliding !== null) {
|
|
285
|
+
if (mode === "409")
|
|
286
|
+
return send(res, 409, { error_key: "conflict", status: 409 });
|
|
287
|
+
if (mode === "201") {
|
|
288
|
+
/* accept */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (typeof body.email === "string")
|
|
292
|
+
row.email = body.email;
|
|
293
|
+
if (typeof body.name === "string")
|
|
294
|
+
row.name = body.name;
|
|
295
|
+
if (typeof body.slug === "string")
|
|
296
|
+
row.slug = body.slug;
|
|
297
|
+
return send(res, 200, row);
|
|
298
|
+
}
|
|
299
|
+
if (item !== null && method === "DELETE") {
|
|
300
|
+
rows.delete(decodeURIComponent(item[1] ?? ""));
|
|
301
|
+
return send(res, 204);
|
|
302
|
+
}
|
|
303
|
+
return send(res, 404, { error_key: "not_found" });
|
|
304
|
+
})().catch(() => {
|
|
305
|
+
if (!res.headersSent)
|
|
306
|
+
send(res, 500, { error_key: "internal" });
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
await new Promise((resolve) => {
|
|
310
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
311
|
+
});
|
|
312
|
+
const addr = server.address();
|
|
313
|
+
return {
|
|
314
|
+
close: () => new Promise((resolve, reject) => {
|
|
315
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
316
|
+
}),
|
|
317
|
+
posts,
|
|
318
|
+
url: `http://127.0.0.1:${addr.port}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const PRINCIPALS = [{ headers: { authorization: "Bearer tok_alpha" }, id: "alpha", roots: { org_id: "org_1" } }];
|
|
322
|
+
async function against(options) {
|
|
323
|
+
const server = await serveHarness(options);
|
|
324
|
+
try {
|
|
325
|
+
const result = await run({
|
|
326
|
+
baseUrl: server.url,
|
|
327
|
+
only: ["widget"],
|
|
328
|
+
principals: PRINCIPALS,
|
|
329
|
+
seed: 1,
|
|
330
|
+
spec: `${server.url}/v1/openapi/spec`,
|
|
331
|
+
});
|
|
332
|
+
return { posts: server.posts, result };
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await server.close();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function defects(findings) {
|
|
339
|
+
return findings.filter((f) => f.verdict !== "COVERAGE_GAP" && f.verdict !== "BLOCKED");
|
|
340
|
+
}
|
|
341
|
+
export async function runUniqueSuite() {
|
|
342
|
+
const results = [];
|
|
343
|
+
const push = (name, why, ok, detail) => {
|
|
344
|
+
results.push({ detail, name, ok, why });
|
|
345
|
+
};
|
|
346
|
+
const shapes = [
|
|
347
|
+
["list of sets", [["email"], ["workspace_id", "slug"]]],
|
|
348
|
+
["column objects", [{ columns: ["email"] }, { columns: ["workspace_id", "slug"] }]],
|
|
349
|
+
["single string[]", ["email"]],
|
|
350
|
+
];
|
|
351
|
+
const expectedList = [
|
|
352
|
+
readUnique({ "x-unique": [["email"], ["workspace_id", "slug"]] }),
|
|
353
|
+
readUnique({ "x-unique": [{ columns: ["email"] }, { columns: ["workspace_id", "slug"] }] }),
|
|
354
|
+
readUnique({ "x-unique": ["email"] }),
|
|
355
|
+
];
|
|
356
|
+
push("accepted x-unique shapes parse to column sets", "list of sets, {columns}, and a single string[] are the documented forms", JSON.stringify(expectedList[0]) === JSON.stringify([["email"], ["workspace_id", "slug"]]) &&
|
|
357
|
+
JSON.stringify(expectedList[1]) === JSON.stringify([["email"], ["workspace_id", "slug"]]) &&
|
|
358
|
+
JSON.stringify(expectedList[2]) === JSON.stringify([["email"]]), shapes.map(([name], i) => `${name}=${JSON.stringify(expectedList[i])}`).join("; "));
|
|
359
|
+
push("empty sets are dropped", "[[]] after filtering is explicit none", JSON.stringify(readUnique({ "x-unique": [[], []] })) === "[]", JSON.stringify(readUnique({ "x-unique": [[], []] })));
|
|
360
|
+
const { doc: malformed } = dereference({
|
|
361
|
+
info: { title: "malformed", version: "1" },
|
|
362
|
+
openapi: "3.1.0",
|
|
363
|
+
paths: {
|
|
364
|
+
"/v1/things": {
|
|
365
|
+
post: {
|
|
366
|
+
operationId: "thing.create",
|
|
367
|
+
responses: { "201": { description: "ok" } },
|
|
368
|
+
"x-entity": { action: "create", identity: "id", name: "thing" },
|
|
369
|
+
"x-unique": { not: "an array" },
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
const malformedModel = buildModel(malformed);
|
|
375
|
+
const uniqueGap = malformedModel.gaps.gaps.some((gap) => gap.tag === "x-unique");
|
|
376
|
+
push("malformed x-unique is absent plus a doctor gap", "non-array is not a guess", malformedModel.byOperationId.get("thing.create")?.unique === null && uniqueGap, `unique=${String(malformedModel.byOperationId.get("thing.create")?.unique)} gap=${String(uniqueGap)}`);
|
|
377
|
+
const tagged = buildModel(widgetSpec({ tag: [["name"], ["email"]] }));
|
|
378
|
+
const planText = report.plan(tagged, false);
|
|
379
|
+
const doctor = report.doctor(tagged, [], false);
|
|
380
|
+
push("plan lists x-unique", "doctor / plan should show the effective sets", planText.includes("x-unique:") && planText.includes("[name]") && tagged.entities.get("widget")?.unique !== null, planText.includes("x-unique:")
|
|
381
|
+
? formatUniqueSets(tagged.entities.get("widget")?.unique ?? null)
|
|
382
|
+
: "missing from plan");
|
|
383
|
+
push("doctor lists x-unique", "doctor / plan should show the effective sets", doctor.text.includes("x-unique:") && doctor.text.includes("[name]"), doctor.text.includes("x-unique:") ? "shown" : "missing from doctor");
|
|
384
|
+
const empty = buildModel(widgetSpec({ tag: [] }));
|
|
385
|
+
push("[] after filter does not run unique checks", "explicit none", JSON.stringify(empty.byOperationId.get("widget.create")?.unique) === "[]", JSON.stringify(empty.byOperationId.get("widget.create")?.unique));
|
|
386
|
+
try {
|
|
387
|
+
const { result } = await against({ spec: { omitTag: true } });
|
|
388
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected");
|
|
389
|
+
push("missing tag does not run unique checks", "do not infer uniqueness", !ran, ran ? "create.unique-conflict-rejected ran" : "absent from checksRun");
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
push("missing tag does not run unique checks", "do not infer uniqueness", false, error instanceof Error ? error.message : String(error));
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const { result } = await against({ duplicate: "409", spec: {}, updateDuplicate: "409" });
|
|
396
|
+
const uniqueBug = defects(result.findings).filter((f) => f.check === "create.unique-conflict-rejected");
|
|
397
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected");
|
|
398
|
+
push("409 duplicate create is not BACKEND_BUG", "the backend matched the document", ran && uniqueBug.length === 0, `ran=${String(ran)} bugs=${uniqueBug.map((f) => f.verdict).join(",") || "none"}`);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
push("409 duplicate create is not BACKEND_BUG", "the backend matched the document", false, error instanceof Error ? error.message : String(error));
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const { result } = await against({ duplicate: "201", spec: {}, updateDuplicate: "ok" });
|
|
405
|
+
const hit = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
406
|
+
push("201 duplicate create is BACKEND_BUG", "a unique constraint that inserts is a backend bug", hit !== undefined, hit === undefined ? result.findings.map((f) => `${f.verdict}:${f.check}`).join(",") || "no finding" : hit.detail);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
push("201 duplicate create is BACKEND_BUG", "a unique constraint that inserts is a backend bug", false, error instanceof Error ? error.message : String(error));
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const { result } = await against({ duplicate: "409", spec: {} });
|
|
413
|
+
const grew = result.findings.some((f) => f.check === "create.unique-conflict-rejected" && f.summary.includes("grew"));
|
|
414
|
+
push("list does not grow on 409", "cardinality is part of the unique pass", !grew, grew ? "cardinality finding" : "no growth finding");
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
push("list does not grow on 409", "cardinality is part of the unique pass", false, error instanceof Error ? error.message : String(error));
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const { result } = await against({
|
|
421
|
+
duplicate: "per-set",
|
|
422
|
+
spec: { tag: [["name"], ["slug"]] },
|
|
423
|
+
updateDuplicate: "ok",
|
|
424
|
+
});
|
|
425
|
+
const hit = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
426
|
+
push("one 2xx set fails the check (not OR'd)", "probe each set separately", hit !== undefined, hit === undefined ? "no BACKEND_BUG" : hit.detail);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
push("one 2xx set fails the check (not OR'd)", "probe each set separately", false, error instanceof Error ? error.message : String(error));
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const { result } = await against({
|
|
433
|
+
spec: { generated: ["id"], omitTag: false, tag: [["id"]] },
|
|
434
|
+
});
|
|
435
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected");
|
|
436
|
+
push("generated-only unique set is skipped", "a set with no body columns is not a fail", !ran, ran ? "ran" : "skipped");
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
push("generated-only unique set is skipped", "a set with no body columns is not a fail", false, error instanceof Error ? error.message : String(error));
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const { result } = await against({
|
|
443
|
+
spec: {
|
|
444
|
+
generated: ["id", "workspace_id"],
|
|
445
|
+
immutable: ["workspace_id"],
|
|
446
|
+
tag: [["workspace_id"]],
|
|
447
|
+
updateBody: ["workspace_id", "name"],
|
|
448
|
+
},
|
|
449
|
+
updateDuplicate: "ok",
|
|
450
|
+
});
|
|
451
|
+
const ran = result.checksRun.includes("update.unique-conflict-rejected");
|
|
452
|
+
push("all-x-immutable update set is skipped", "immutable unique columns are not a unique PATCH probe", !ran, ran ? "ran" : "skipped");
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
push("all-x-immutable update set is skipped", "immutable unique columns are not a unique PATCH probe", false, error instanceof Error ? error.message : String(error));
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const { result } = await against({ duplicate: "409", spec: {}, updateDuplicate: "409" });
|
|
459
|
+
const bug = result.findings.find((f) => f.check === "update.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
460
|
+
const ran = result.checksRun.includes("update.unique-conflict-rejected");
|
|
461
|
+
push("colliding PATCH 409 passes", "same scoring as create", ran && bug === undefined, `ran=${String(ran)} bug=${bug?.detail ?? "none"}`);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
push("colliding PATCH 409 passes", "same scoring as create", false, error instanceof Error ? error.message : String(error));
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const { result } = await against({ duplicate: "409", spec: {}, updateDuplicate: "201" });
|
|
468
|
+
const hit = result.findings.find((f) => f.check === "update.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
469
|
+
push("colliding PATCH 2xx is BACKEND_BUG", "update unique is the same class as create", hit !== undefined, hit === undefined ? "no finding" : hit.detail);
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
push("colliding PATCH 2xx is BACKEND_BUG", "update unique is the same class as create", false, error instanceof Error ? error.message : String(error));
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const { result, posts } = await against({ duplicate: "409", spec: { idempotencyRequired: true } });
|
|
476
|
+
const uniquePosts = posts.filter((_, i) => i > 0);
|
|
477
|
+
const reused = uniquePosts.some((post) => {
|
|
478
|
+
const value = post.headers["idempotency-key"];
|
|
479
|
+
const first = posts[0]?.headers["idempotency-key"];
|
|
480
|
+
return typeof value === "string" && typeof first === "string" && value === first && value !== "";
|
|
481
|
+
});
|
|
482
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected");
|
|
483
|
+
push("unique probe does not reuse Idempotency-Key", "omit or send a fresh key", ran && !reused, reused ? "reused seed key" : "fresh or omitted");
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
push("unique probe does not reuse Idempotency-Key", "omit or send a fresh key", false, error instanceof Error ? error.message : String(error));
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const { result } = await against({
|
|
490
|
+
duplicate: "409-drift",
|
|
491
|
+
spec: {},
|
|
492
|
+
updateDuplicate: "ok",
|
|
493
|
+
});
|
|
494
|
+
const drift = result.findings.find((f) => f.check === "schema.error-response-matches-document");
|
|
495
|
+
push("409 schema drift still reports", "coverage is not a free pass for an undeclared body", drift !== undefined, drift === undefined ? result.findings.map((f) => f.check).join(",") || "none" : drift.verdict);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
push("409 schema drift still reports", "coverage is not a free pass for an undeclared body", false, error instanceof Error ? error.message : String(error));
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const { result } = await against({
|
|
502
|
+
duplicate: "gate-403",
|
|
503
|
+
spec: { featureGate: "widgets" },
|
|
504
|
+
updateDuplicate: "ok",
|
|
505
|
+
});
|
|
506
|
+
const uniqueBug = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
507
|
+
const gap = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "COVERAGE_GAP");
|
|
508
|
+
push("feature-gate 403 on the probe is coverage, not a unique pass", "do not score a documented 403 as 409", uniqueBug === undefined && gap !== undefined, `bug=${uniqueBug?.verdict ?? "none"} gap=${gap?.verdict ?? "none"}`);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
push("feature-gate 403 on the probe is coverage, not a unique pass", "do not score a documented 403 as 409", false, error instanceof Error ? error.message : String(error));
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const { result } = await against({ duplicate: "402", spec: {}, updateDuplicate: "ok" });
|
|
515
|
+
const uniqueBug = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "BACKEND_BUG");
|
|
516
|
+
const gap = result.findings.find((f) => f.check === "create.unique-conflict-rejected" && f.verdict === "COVERAGE_GAP");
|
|
517
|
+
push("402 on the probe is coverage, not a unique pass", "plan limit is not a unique 409", uniqueBug === undefined && gap !== undefined, `bug=${uniqueBug?.verdict ?? "none"} gap=${gap?.verdict ?? "none"}`);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
push("402 on the probe is coverage, not a unique pass", "plan limit is not a unique 409", false, error instanceof Error ? error.message : String(error));
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const { result } = await against({
|
|
524
|
+
duplicate: "409",
|
|
525
|
+
listSeeded: true,
|
|
526
|
+
seedConflict: true,
|
|
527
|
+
spec: {},
|
|
528
|
+
updateDuplicate: "ok",
|
|
529
|
+
});
|
|
530
|
+
const seed = result.findings.find((f) => f.check === "world.seed" && f.entity === "widget");
|
|
531
|
+
const blocked = seed?.verdict === "BLOCKED";
|
|
532
|
+
const namesTag = seed?.detail.includes("x-unique") === true;
|
|
533
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected");
|
|
534
|
+
const uniquePassAsSeed = result.findings.some((f) => f.check === "create.unique-conflict-rejected" && f.verdict !== "BACKEND_BUG" && f.evidence.length === 0);
|
|
535
|
+
push("tagged seed 409 + nonempty list adopts", "world.seed is COVERAGE_GAP naming x-unique, not BLOCKED", seed?.verdict === "COVERAGE_GAP" && namesTag && !blocked, `seed=${seed?.verdict ?? "none"} ${seed?.detail ?? ""}`);
|
|
536
|
+
push("unique check still runs after unique-409 adopt", "do not stand unique probes down with write-path skips", ran, ran ? "ran" : `checksRun=${result.checksRun.join(",")}`);
|
|
537
|
+
push("seed 409 is not the unique check passing", "that check is the explicit second POST", !uniquePassAsSeed, uniquePassAsSeed ? "scored seed as unique pass" : "seed is not unique-check evidence");
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
541
|
+
push("tagged seed 409 + nonempty list adopts", "world.seed is COVERAGE_GAP naming x-unique, not BLOCKED", false, msg);
|
|
542
|
+
push("unique check still runs after unique-409 adopt", "do not stand unique probes down with write-path skips", false, msg);
|
|
543
|
+
push("seed 409 is not the unique check passing", "that check is the explicit second POST", false, msg);
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const { result } = await against({ seedConflict: true, spec: {} });
|
|
547
|
+
const seed = result.findings.find((f) => f.check === "world.seed" && f.entity === "widget");
|
|
548
|
+
push("tagged seed 409 + empty list is BLOCKED", "could not seed", seed?.verdict === "BLOCKED", `seed=${seed?.verdict ?? "none"} ${seed?.detail ?? ""}`);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
push("tagged seed 409 + empty list is BLOCKED", "could not seed", false, error instanceof Error ? error.message : String(error));
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
const { result } = await against({ seedConflict: true, spec: { omitTag: true } });
|
|
555
|
+
const seed = result.findings.find((f) => f.check === "world.seed" && f.entity === "widget");
|
|
556
|
+
push("untagged seed 409 is still a seed failure", "do not treat every 409 as unique", seed?.verdict === "BLOCKED", `seed=${seed?.verdict ?? "none"} ${seed?.detail ?? ""}`);
|
|
557
|
+
const untaggedAdopts = seed?.verdict === "COVERAGE_GAP" && seed.detail.includes("x-unique");
|
|
558
|
+
push("untagged 409 does not adopt as x-unique", "no tag, no unique adopt", !untaggedAdopts, untaggedAdopts ? "adopted without a tag" : "not unique-adopt");
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
562
|
+
push("untagged seed 409 is still a seed failure", "do not treat every 409 as unique", false, msg);
|
|
563
|
+
push("untagged 409 does not adopt as x-unique", "no tag, no unique adopt", false, msg);
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const { result } = await against({ duplicate: "409", spec: {} });
|
|
567
|
+
const uniqueBug = defects(result.findings).filter((f) => f.check.startsWith("create.unique") || f.check.startsWith("update.unique"));
|
|
568
|
+
push("409 duplicate and 201 distinct passes", "the cohort still inserts distinct unique values", uniqueBug.length === 0 && result.entitiesTested.includes("widget"), `bugs=${uniqueBug.map((f) => f.check).join(",") || "none"} entities=${result.entitiesTested.join(",")}`);
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
push("409 duplicate and 201 distinct passes", "the cohort still inserts distinct unique values", false, error instanceof Error ? error.message : String(error));
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
const { result } = await against({ spec: { tag: [] } });
|
|
575
|
+
const ran = result.checksRun.includes("create.unique-conflict-rejected") ||
|
|
576
|
+
result.checksRun.includes("update.unique-conflict-rejected");
|
|
577
|
+
push("explicit empty x-unique does not run the check", "[] after filter is none", !ran, ran ? "ran" : "absent");
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
push("explicit empty x-unique does not run the check", "[] after filter is none", false, error instanceof Error ? error.message : String(error));
|
|
581
|
+
}
|
|
582
|
+
const missingDoctor = report.doctor(buildModel(widgetSpec({ omitTag: true })), [], false);
|
|
583
|
+
push("doctor says unique checks do not run without the tag", "TAG_UNLOCKS names the locked checks", missingDoctor.text.includes("x-unique") && missingDoctor.text.includes("create.unique-conflict-rejected"), missingDoctor.text.includes("x-unique") ? "named" : "missing from doctor");
|
|
584
|
+
return results;
|
|
585
|
+
}
|
|
586
|
+
//# sourceMappingURL=unique.js.map
|