@homeflare/alchemy 0.27.4 → 0.29.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 +2 -0
- package/dist/caddy/admin-address.d.ts +27 -0
- package/dist/caddy/admin-address.d.ts.map +1 -0
- package/dist/caddy/admin-calls.d.ts +25 -28
- package/dist/caddy/admin-calls.d.ts.map +1 -1
- package/dist/caddy/admin.d.ts +28 -38
- package/dist/caddy/admin.d.ts.map +1 -1
- package/dist/caddy/caddy-http-client.d.ts +25 -0
- package/dist/caddy/caddy-http-client.d.ts.map +1 -0
- package/dist/caddy/config-errors.d.ts +24 -0
- package/dist/caddy/config-errors.d.ts.map +1 -0
- package/dist/caddy/config-lifecycle.d.ts +20 -49
- package/dist/caddy/config-lifecycle.d.ts.map +1 -1
- package/dist/caddy/config-reconcile.d.ts +45 -0
- package/dist/caddy/config-reconcile.d.ts.map +1 -0
- package/dist/caddy/config.d.ts +4 -3
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/fake-caddy.d.ts +10 -2
- package/dist/caddy/fake-caddy.d.ts.map +1 -1
- package/dist/caddy/index.d.ts +3 -3
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +487 -444
- package/dist/caddy/index.js.map +16 -12
- package/dist/caddy/local-admin.d.ts +20 -34
- package/dist/caddy/local-admin.d.ts.map +1 -1
- package/dist/caddy/providers.d.ts +19 -3
- package/dist/caddy/providers.d.ts.map +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
- package/dist/forgejo/branch-protection.d.ts +4 -4
- package/dist/forgejo/org-actions-secrets.d.ts +4 -4
- package/dist/forgejo/org-label.d.ts +4 -4
- package/dist/forgejo/org-team.d.ts +4 -4
- package/dist/forgejo/repo-webhook.d.ts +4 -4
- package/dist/forgejo/repository.d.ts +4 -4
- package/dist/forgejo/team-member.d.ts +4 -4
- package/dist/github/declare-repo-baseline.d.ts +1 -1
- package/dist/github/declare-repo-baseline.d.ts.map +1 -1
- package/dist/github/index.d.ts +9 -5
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +161 -25
- package/dist/github/index.js.map +15 -13
- package/dist/github/repo-baseline-data.d.ts +10 -1
- package/dist/github/repo-baseline-data.d.ts.map +1 -1
- package/dist/github/repo-policy.d.ts +1 -1
- package/dist/github/repo-policy.d.ts.map +1 -1
- package/dist/github/repository-ruleset-errors.d.ts +37 -3
- package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
- package/dist/github/repository-ruleset-form.d.ts.map +1 -1
- package/dist/github/repository-ruleset-guards.d.ts +15 -2
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
- package/dist/github/repository-ruleset-narrowing-guards.d.ts +39 -0
- package/dist/github/repository-ruleset-narrowing-guards.d.ts.map +1 -0
- package/dist/github/repository-ruleset-octokit.d.ts +49 -0
- package/dist/github/repository-ruleset-octokit.d.ts.map +1 -1
- package/dist/github/repository-ruleset-probe.d.ts +2 -1
- package/dist/github/repository-ruleset-probe.d.ts.map +1 -1
- package/dist/github/repository-ruleset-props.d.ts +91 -0
- package/dist/github/repository-ruleset-props.d.ts.map +1 -0
- package/dist/github/repository-ruleset-providers.d.ts +42 -0
- package/dist/github/repository-ruleset-providers.d.ts.map +1 -0
- package/dist/github/repository-ruleset-reconcile.d.ts.map +1 -1
- package/dist/github/repository-ruleset.d.ts +28 -34
- package/dist/github/repository-ruleset.d.ts.map +1 -1
- package/dist/litellm/fake-litellm.d.ts +27 -16
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/fake-stack.d.ts +9 -4
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/index.d.ts +7 -5
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +53 -123
- package/dist/litellm/index.js.map +8 -8
- package/dist/litellm/operations.d.ts +60 -0
- package/dist/litellm/operations.d.ts.map +1 -0
- package/dist/litellm/pass-through-endpoint.d.ts +10 -28
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
- package/dist/litellm/pass-through-errors.d.ts +37 -0
- package/dist/litellm/pass-through-errors.d.ts.map +1 -0
- package/dist/litellm/pass-through-form.d.ts +11 -4
- package/dist/litellm/pass-through-form.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +7 -5
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/netbox/prefix.d.ts +4 -4
- package/dist/paperless/custom-field.d.ts +21 -21
- package/dist/paperless/custom-field.d.ts.map +1 -1
- package/dist/paperless/document-type.d.ts +34 -4
- package/dist/paperless/document-type.d.ts.map +1 -1
- package/dist/paperless/fake-paperless.d.ts +36 -19
- package/dist/paperless/fake-paperless.d.ts.map +1 -1
- package/dist/paperless/index.d.ts +7 -2
- package/dist/paperless/index.d.ts.map +1 -1
- package/dist/paperless/index.js +164 -228
- package/dist/paperless/index.js.map +10 -13
- package/dist/paperless/matching-locate.d.ts +42 -20
- package/dist/paperless/matching-locate.d.ts.map +1 -1
- package/dist/paperless/matching-types.d.ts +47 -18
- package/dist/paperless/matching-types.d.ts.map +1 -1
- package/dist/paperless/matching.d.ts +30 -27
- package/dist/paperless/matching.d.ts.map +1 -1
- package/dist/paperless/providers.d.ts +13 -7
- package/dist/paperless/providers.d.ts.map +1 -1
- package/dist/paperless/storage-path.d.ts +34 -4
- package/dist/paperless/storage-path.d.ts.map +1 -1
- package/dist/paperless/tag.d.ts +43 -8
- package/dist/paperless/tag.d.ts.map +1 -1
- package/dist/proxmox/acl.d.ts +30 -23
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/distilled-pve.d.ts +89 -0
- package/dist/proxmox/distilled-pve.d.ts.map +1 -0
- package/dist/proxmox/index.js +663 -609
- package/dist/proxmox/index.js.map +12 -11
- package/dist/proxmox/members.d.ts +29 -10
- package/dist/proxmox/members.d.ts.map +1 -1
- package/docs/caddy-admin.md +1 -1
- package/docs/caddy.md +3 -2
- package/docs/credentials.md +16 -0
- package/docs/litellm.md +21 -13
- package/docs/paperless.md +12 -0
- package/docs/peers.md +5 -0
- package/docs/repo-policy.md +16 -1
- package/docs/repository-ruleset.md +53 -13
- package/docs/upstream-conformance.md +126 -10
- package/package.json +9 -2
- package/src/caddy/admin-address.ts +71 -0
- package/src/caddy/admin-calls.ts +48 -100
- package/src/caddy/admin.ts +37 -41
- package/src/caddy/adopt-scope.test.ts +1 -1
- package/src/caddy/caddy-http-client.ts +229 -0
- package/src/caddy/config-adopt-provider.test.ts +139 -0
- package/src/caddy/config-adopt.test.ts +60 -126
- package/src/caddy/config-errors.ts +47 -0
- package/src/caddy/config-lifecycle.test.ts +55 -42
- package/src/caddy/config-lifecycle.ts +72 -162
- package/src/caddy/config-reconcile.ts +138 -0
- package/src/caddy/config.ts +36 -42
- package/src/caddy/fake-caddy.ts +20 -2
- package/src/caddy/index.ts +3 -8
- package/src/caddy/local-admin.test.ts +35 -21
- package/src/caddy/local-admin.ts +60 -167
- package/src/caddy/providers.test.ts +14 -6
- package/src/caddy/providers.ts +20 -4
- package/src/github/declare-repo-baseline.ts +6 -6
- package/src/github/index.ts +10 -4
- package/src/github/repo-baseline-data.test.ts +13 -0
- package/src/github/repo-baseline-data.ts +11 -2
- package/src/github/repo-policy-stack.test.ts +59 -0
- package/src/github/repo-policy.ts +3 -2
- package/src/github/repository-ruleset-errors.ts +51 -4
- package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
- package/src/github/repository-ruleset-exact-adopt.test.ts +184 -0
- package/src/github/repository-ruleset-form.ts +7 -3
- package/src/github/repository-ruleset-guards.ts +11 -4
- package/src/github/repository-ruleset-narrowing-guards.ts +141 -0
- package/src/github/repository-ruleset-narrowing.test.ts +210 -0
- package/src/github/repository-ruleset-octokit.test.ts +141 -0
- package/src/github/repository-ruleset-octokit.ts +121 -30
- package/src/github/repository-ruleset-probe.ts +2 -1
- package/src/github/repository-ruleset-props.ts +95 -0
- package/src/github/repository-ruleset-providers.ts +46 -0
- package/src/github/repository-ruleset-reconcile.ts +35 -0
- package/src/github/repository-ruleset-wire.test.ts +38 -27
- package/src/github/repository-ruleset-write-refusals.test.ts +6 -26
- package/src/github/repository-ruleset.ts +53 -43
- package/src/litellm/fake-litellm.ts +83 -78
- package/src/litellm/fake-stack.ts +13 -5
- package/src/litellm/index.ts +7 -21
- package/src/litellm/operations.ts +149 -0
- package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +17 -20
- package/src/litellm/pass-through-endpoint-replace.test.ts +3 -4
- package/src/litellm/pass-through-endpoint.test.ts +8 -8
- package/src/litellm/pass-through-endpoint.ts +21 -31
- package/src/litellm/pass-through-errors.ts +37 -0
- package/src/litellm/pass-through-form.ts +20 -10
- package/src/litellm/providers.ts +8 -7
- package/src/paperless/custom-field.test.ts +49 -40
- package/src/paperless/custom-field.ts +57 -33
- package/src/paperless/document-type.test.ts +48 -0
- package/src/paperless/document-type.ts +51 -22
- package/src/paperless/fake-paperless.ts +63 -72
- package/src/paperless/index.ts +7 -13
- package/src/paperless/matching-locate.test.ts +102 -0
- package/src/paperless/matching-locate.ts +80 -86
- package/src/paperless/matching-types.ts +49 -16
- package/src/paperless/matching.ts +77 -90
- package/src/paperless/providers.ts +14 -10
- package/src/paperless/storage-path.test.ts +56 -0
- package/src/paperless/storage-path.ts +53 -24
- package/src/paperless/tag-identity.test.ts +127 -0
- package/src/paperless/tag.test.ts +100 -0
- package/src/paperless/tag.ts +62 -32
- package/src/proxmox/acl.test.ts +145 -0
- package/src/proxmox/acl.ts +128 -150
- package/src/proxmox/distilled-pve.test.ts +231 -0
- package/src/proxmox/distilled-pve.ts +147 -0
- package/src/proxmox/members-timeout.test.ts +161 -0
- package/src/proxmox/members.test.ts +9 -0
- package/src/proxmox/members.ts +36 -2
- package/dist/litellm/client.d.ts +0 -76
- package/dist/litellm/client.d.ts.map +0 -1
- package/dist/litellm/credentials.d.ts +0 -48
- package/dist/litellm/credentials.d.ts.map +0 -1
- package/dist/litellm/generated/pass-through.d.ts +0 -71
- package/dist/litellm/generated/pass-through.d.ts.map +0 -1
- package/dist/paperless/client.d.ts +0 -37
- package/dist/paperless/client.d.ts.map +0 -1
- package/dist/paperless/credentials.d.ts +0 -40
- package/dist/paperless/credentials.d.ts.map +0 -1
- package/dist/paperless/errors.d.ts +0 -49
- package/dist/paperless/errors.d.ts.map +0 -1
- package/src/litellm/client.ts +0 -195
- package/src/litellm/credentials.ts +0 -73
- package/src/litellm/generated/pass-through.ts +0 -73
- package/src/paperless/client.ts +0 -88
- package/src/paperless/credentials.ts +0 -69
- package/src/paperless/errors.ts +0 -62
- package/src/paperless/matching-identity.test.ts +0 -194
- package/src/paperless/matching.test.ts +0 -128
package/src/paperless/errors.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paperless-ngx's HTTP status codes, mapped to tagged errors exactly once — here, and never
|
|
3
|
-
* again by a status check inside a resource (S21, alchemy-provider-standard).
|
|
4
|
-
*
|
|
5
|
-
* ⛔ A SINGLE `PaperlessError` CLASS WITH A `.status` FIELD WOULD WORK, AND IS WHAT `netbox/
|
|
6
|
-
* client.ts` DOES. This family does it the S21 way instead: one tag per distinct MEANING, so a
|
|
7
|
-
* caller writes `Effect.catchTag('PaperlessNotFound', …)` rather than `if (e.status === 404)`.
|
|
8
|
-
* `matching.ts`'s "a 403 is never read as absent" rule is exactly what a shared tag buys —
|
|
9
|
-
* `catchTag('PaperlessNotFound', …)` cannot accidentally also catch `PaperlessUnauthorized`.
|
|
10
|
-
*/
|
|
11
|
-
import * as Data from 'effect/Data';
|
|
12
|
-
|
|
13
|
-
/** No object at that path. The only tag `matching.ts` folds to "absent". */
|
|
14
|
-
export class PaperlessNotFound extends Data.TaggedError('PaperlessNotFound')<{
|
|
15
|
-
readonly method: string;
|
|
16
|
-
readonly path: string;
|
|
17
|
-
}> {}
|
|
18
|
-
|
|
19
|
-
/** 401 (no/bad token) or 403 (token valid, forbidden) — NEVER read as "the object is absent". */
|
|
20
|
-
export class PaperlessUnauthorized extends Data.TaggedError('PaperlessUnauthorized')<{
|
|
21
|
-
readonly status: 401 | 403;
|
|
22
|
-
readonly method: string;
|
|
23
|
-
readonly path: string;
|
|
24
|
-
}> {}
|
|
25
|
-
|
|
26
|
-
/** 400 — the vendor rejected the body. Carries it, so the message quotes Paperless's own words. */
|
|
27
|
-
export class PaperlessValidation extends Data.TaggedError('PaperlessValidation')<{
|
|
28
|
-
readonly method: string;
|
|
29
|
-
readonly path: string;
|
|
30
|
-
readonly body: string;
|
|
31
|
-
}> {}
|
|
32
|
-
|
|
33
|
-
/** 5xx, or a status this family's endpoints do not otherwise document. */
|
|
34
|
-
export class PaperlessUnavailable extends Data.TaggedError('PaperlessUnavailable')<{
|
|
35
|
-
readonly status: number;
|
|
36
|
-
readonly method: string;
|
|
37
|
-
readonly path: string;
|
|
38
|
-
readonly detail: string;
|
|
39
|
-
}> {}
|
|
40
|
-
|
|
41
|
-
export type PaperlessError =
|
|
42
|
-
| PaperlessNotFound
|
|
43
|
-
| PaperlessUnauthorized
|
|
44
|
-
| PaperlessValidation
|
|
45
|
-
| PaperlessUnavailable;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The one place a status code is read. ⚠️ Anything outside {400,401,403,404,5xx} — 405, 409, 429
|
|
49
|
-
* — is not documented for these four endpoints, so it falls to `PaperlessValidation` rather than
|
|
50
|
-
* inventing a fifth tag for a case that was never measured.
|
|
51
|
-
*/
|
|
52
|
-
export const statusToError = (
|
|
53
|
-
status: number,
|
|
54
|
-
method: string,
|
|
55
|
-
path: string,
|
|
56
|
-
body: string,
|
|
57
|
-
): PaperlessError => {
|
|
58
|
-
if (status === 404) return new PaperlessNotFound({ method, path });
|
|
59
|
-
if (status === 401 || status === 403) return new PaperlessUnauthorized({ method, path, status });
|
|
60
|
-
if (status >= 500) return new PaperlessUnavailable({ detail: body, method, path, status });
|
|
61
|
-
return new PaperlessValidation({ body, method, path });
|
|
62
|
-
};
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression suite for the PR 163 red-team HIGH finding: `diff`/`reconcile` re-derived identity
|
|
3
|
-
* from `news` alone on every call, so renaming (or re-owning) an already-deployed Tag/
|
|
4
|
-
* DocumentType/StoragePath/CustomField missed the row it previously wrote, POSTed a second
|
|
5
|
-
* object under the new name/owner, and left the original permanently orphaned — while `diff`
|
|
6
|
-
* reported a plain `{action:'update'}`. See `matching.ts`'s header for the fix: once `output` is
|
|
7
|
-
* defined, identity is `output.id`, not `(name, owner)`.
|
|
8
|
-
*
|
|
9
|
-
* ⛔ EVERY TEST HERE FAILS ON THE PRE-FIX `matching.ts` (measured before the fix landed — see the
|
|
10
|
-
* PR comment this file was introduced with):
|
|
11
|
-
* - the two rename/re-own tests fail because a second POST fires and `fake.rows('tags').length`
|
|
12
|
-
* is 2, not 1 (the exact duplicate-and-orphan bug);
|
|
13
|
-
* - the deleted-out-of-band test fails because `diff` never called `fetchLive` at all when
|
|
14
|
-
* `output` was ignored the pre-fix way — it silently returns `{action:'update'}` instead of
|
|
15
|
-
* refusing;
|
|
16
|
-
* - the first-create and adopt tests already passed pre-fix (the no-state path was never
|
|
17
|
-
* broken) and are here to prove the fix does not disturb them.
|
|
18
|
-
*
|
|
19
|
-
* ★ THE LAST `describe` BELOW IS A SECOND ROUND (re-review of this same PR): the d3983ff fix
|
|
20
|
-
* rewired `diff`/`reconcile` to locate by `output.id`, but left `destroy` locating by
|
|
21
|
-
* `(name, owner)` alone — the exact pre-fix mechanism, with the exact pre-fix consequence
|
|
22
|
-
* (silently missing a live-but-renamed row) just on the delete path instead of update. See
|
|
23
|
-
* `matching.ts`'s `destroy` for the fix this suite proves.
|
|
24
|
-
*/
|
|
25
|
-
import { describe, expect, test } from 'bun:test';
|
|
26
|
-
import { paperless } from './client.ts';
|
|
27
|
-
import { type MatchingProps, matchingHandlers, matchingOperations } from './matching.ts';
|
|
28
|
-
import { run, withFake } from './fake-paperless.ts';
|
|
29
|
-
|
|
30
|
-
interface Props extends MatchingProps {
|
|
31
|
-
note?: string;
|
|
32
|
-
}
|
|
33
|
-
/** ★ The same minimal spec `matching.test.ts` uses — this suite tests `matching.ts`'s own identity contract, not any one family's wiring. */
|
|
34
|
-
const spec = {
|
|
35
|
-
attributes: (live: Record<string, unknown>, props: Props) => {
|
|
36
|
-
const id = live['id'];
|
|
37
|
-
if (typeof id !== 'number') return undefined;
|
|
38
|
-
return {
|
|
39
|
-
id,
|
|
40
|
-
name: typeof live['name'] === 'string' ? live['name'] : props.name,
|
|
41
|
-
note: typeof live['note'] === 'string' ? live['note'] : '',
|
|
42
|
-
owner: typeof live['owner'] === 'number' ? live['owner'] : undefined,
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
collection: 'tags' as const,
|
|
46
|
-
describe: (props: Props) => `tags ${props.name}`,
|
|
47
|
-
endpoint: { create: 'paperless:POST /api/tags/', update: 'paperless:PATCH /api/tags/{id}/' },
|
|
48
|
-
fields: [
|
|
49
|
-
{ key: 'note', live: (r: Record<string, unknown>) => r['note'], prop: (p: Props) => p.note },
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const writesOf = (fake: { seen: readonly { method: string }[] }) =>
|
|
54
|
-
fake.seen.filter((s) => s.method !== 'GET');
|
|
55
|
-
|
|
56
|
-
describe('renaming or re-owning an already-deployed object (PR 163)', () => {
|
|
57
|
-
test('a rename PATCHes the same id — no POST, no orphan', async () => {
|
|
58
|
-
await withFake(async (fake) => {
|
|
59
|
-
const handlers = matchingHandlers(spec);
|
|
60
|
-
const created = await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
61
|
-
const before = writesOf(fake).length;
|
|
62
|
-
|
|
63
|
-
const renamed = await run(
|
|
64
|
-
handlers.reconcile({ news: { name: 'Invoices - Renamed' }, output: created }),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const writes = writesOf(fake);
|
|
68
|
-
const newWrites = writes.slice(before);
|
|
69
|
-
expect(writes.length).toBe(before + 1);
|
|
70
|
-
expect(newWrites.every((w) => w.method === 'PATCH')).toBe(true);
|
|
71
|
-
expect(fake.rows('tags').length).toBe(1);
|
|
72
|
-
expect(renamed?.id).toBe(created?.id);
|
|
73
|
-
expect(renamed?.name).toBe('Invoices - Renamed');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('an owner change PATCHes the same id — no POST, no orphan', async () => {
|
|
78
|
-
await withFake(async (fake) => {
|
|
79
|
-
const handlers = matchingHandlers(spec);
|
|
80
|
-
const created = await run(handlers.reconcile({ news: { name: 'Invoices', owner: 7 } }));
|
|
81
|
-
const before = writesOf(fake).length;
|
|
82
|
-
|
|
83
|
-
const reowned = await run(
|
|
84
|
-
handlers.reconcile({ news: { name: 'Invoices', owner: 9 }, output: created }),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const writes = writesOf(fake);
|
|
88
|
-
expect(writes.length).toBe(before + 1);
|
|
89
|
-
expect(writes[writes.length - 1]?.method).toBe('PATCH');
|
|
90
|
-
expect(fake.rows('tags').length).toBe(1);
|
|
91
|
-
expect(reowned?.id).toBe(created?.id);
|
|
92
|
-
expect(reowned?.owner).toBe(9);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('a deleted-out-of-band object (PR 163)', () => {
|
|
98
|
-
test('diff refuses at plan rather than creating a replacement', async () => {
|
|
99
|
-
await withFake(async (fake) => {
|
|
100
|
-
const handlers = matchingHandlers(spec);
|
|
101
|
-
const created = await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
102
|
-
await run(paperless('DELETE', `tags/${String(created?.id)}/`));
|
|
103
|
-
|
|
104
|
-
await expect(
|
|
105
|
-
run(handlers.diff({ news: { name: 'Invoices' }, output: created })),
|
|
106
|
-
).rejects.toThrow(/no longer exists live|deleted out of band/);
|
|
107
|
-
|
|
108
|
-
expect(fake.rows('tags').length).toBe(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('reconcile also refuses, not only diff, if called with no prior diff', async () => {
|
|
113
|
-
await withFake(async (fake) => {
|
|
114
|
-
const handlers = matchingHandlers(spec);
|
|
115
|
-
const created = await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
116
|
-
await run(paperless('DELETE', `tags/${String(created?.id)}/`));
|
|
117
|
-
|
|
118
|
-
await expect(
|
|
119
|
-
run(handlers.reconcile({ news: { name: 'Invoices' }, output: created })),
|
|
120
|
-
).rejects.toThrow(/no longer exists live|deleted out of band/);
|
|
121
|
-
|
|
122
|
-
expect(fake.rows('tags').length).toBe(0);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('the no-state path is unaffected (PR 163)', () => {
|
|
128
|
-
test('a first create still locates by name', async () => {
|
|
129
|
-
await withFake(async (fake) => {
|
|
130
|
-
const handlers = matchingHandlers(spec);
|
|
131
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
132
|
-
|
|
133
|
-
const get = fake.seen.find((s) => s.method === 'GET');
|
|
134
|
-
expect(get?.path).toContain('name__iexact=Invoices');
|
|
135
|
-
const post = fake.seen.find((s) => s.method === 'POST');
|
|
136
|
-
expect(post).toBeDefined();
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("adopt (output populated from the engine's own read) locates the id it read, no duplicate", async () => {
|
|
141
|
-
await withFake(async (fake) => {
|
|
142
|
-
const ops = matchingOperations(spec);
|
|
143
|
-
const handlers = matchingHandlers(spec);
|
|
144
|
-
// A pre-existing object, created out of band — what the engine's `provider.read` (always
|
|
145
|
-
// called with `output: undefined`) discovers during its adoption probe (`Plan.ts`).
|
|
146
|
-
await run(paperless('POST', 'tags/', { name: 'Invoices', owner: null }));
|
|
147
|
-
const readAttrs = await run(ops.read({ name: 'Invoices' }));
|
|
148
|
-
expect(readAttrs).toBeDefined();
|
|
149
|
-
if (readAttrs === undefined) throw new Error('unreachable: asserted defined above');
|
|
150
|
-
|
|
151
|
-
// The engine forces `action: 'adopted'` through the normal update/reconcile path with
|
|
152
|
-
// `output: node.state.attr` already the just-read attributes (Apply.ts).
|
|
153
|
-
const after = await run(
|
|
154
|
-
handlers.reconcile({ news: { name: 'Invoices' }, output: readAttrs }),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
expect(fake.rows('tags').length).toBe(1);
|
|
158
|
-
expect(after?.id).toBe(readAttrs?.id);
|
|
159
|
-
expect(fake.seen.filter((s) => s.method === 'POST').length).toBe(1);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('delete locates by output.id, not by (possibly stale) olds.name (re-review)', () => {
|
|
165
|
-
test('a row renamed out of band is still found and deleted, by id', async () => {
|
|
166
|
-
await withFake(async (fake) => {
|
|
167
|
-
const handlers = matchingHandlers(spec);
|
|
168
|
-
const created = await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
169
|
-
if (created === undefined) throw new Error('unreachable: reconcile always returns attrs');
|
|
170
|
-
|
|
171
|
-
// Nothing this package did — a human (or another tool) renamed the live object directly
|
|
172
|
-
// in Paperless. State's `olds` still says the OLD name; `output` still holds the real id.
|
|
173
|
-
await run(paperless('PATCH', `tags/${String(created.id)}/`, { name: 'Renamed Elsewhere' }));
|
|
174
|
-
|
|
175
|
-
await run(handlers.delete({ olds: { name: 'Invoices' }, output: created }));
|
|
176
|
-
|
|
177
|
-
expect(fake.rows('tags').length).toBe(0);
|
|
178
|
-
const deletes = fake.seen.filter((s) => s.method === 'DELETE');
|
|
179
|
-
expect(deletes.length).toBe(1);
|
|
180
|
-
expect(deletes[0]?.path).toBe(`/api/tags/${String(created.id)}/`);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('with no output (no-state path), delete still falls back to locating by name', async () => {
|
|
185
|
-
await withFake(async (fake) => {
|
|
186
|
-
const handlers = matchingHandlers(spec);
|
|
187
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
188
|
-
|
|
189
|
-
await run(handlers.delete({ olds: { name: 'Invoices' } }));
|
|
190
|
-
|
|
191
|
-
expect(fake.rows('tags').length).toBe(0);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `matching.ts`'s generic lifecycle against a fake Paperless: owner injection, the version
|
|
3
|
-
* header, 403-never-absent, case-sensitive identity inside a case-insensitive locate, and
|
|
4
|
-
* Unowned-on-every-match (H1).
|
|
5
|
-
*
|
|
6
|
-
* ⛔ THESE TESTS FAIL WITHOUT THE UNIT. `matching.ts`, `client.ts`, `credentials.ts` and
|
|
7
|
-
* `errors.ts` did not exist before this change — there was nothing to import.
|
|
8
|
-
*/
|
|
9
|
-
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
10
|
-
import { describe, expect, test } from 'bun:test';
|
|
11
|
-
import { run, runFailure, withFake } from './fake-paperless.ts';
|
|
12
|
-
import { type MatchingProps, matchingHandlers, matchingOperations } from './matching.ts';
|
|
13
|
-
|
|
14
|
-
interface Props extends MatchingProps {
|
|
15
|
-
note?: string;
|
|
16
|
-
}
|
|
17
|
-
/** ★ A minimal spec — this file tests `matching.ts`'s own contract, not any one family's wiring. */
|
|
18
|
-
const spec = {
|
|
19
|
-
attributes: (live: Record<string, unknown>, props: Props) => {
|
|
20
|
-
const id = live['id'];
|
|
21
|
-
if (typeof id !== 'number') return undefined;
|
|
22
|
-
return {
|
|
23
|
-
id,
|
|
24
|
-
name: typeof live['name'] === 'string' ? live['name'] : props.name,
|
|
25
|
-
note: typeof live['note'] === 'string' ? live['note'] : '',
|
|
26
|
-
owner: typeof live['owner'] === 'number' ? live['owner'] : undefined,
|
|
27
|
-
};
|
|
28
|
-
},
|
|
29
|
-
collection: 'tags' as const,
|
|
30
|
-
describe: (props: Props) => `tags ${props.name}`,
|
|
31
|
-
endpoint: { create: 'paperless:POST /api/tags/', update: 'paperless:PATCH /api/tags/{id}/' },
|
|
32
|
-
fields: [
|
|
33
|
-
{ key: 'note', live: (r: Record<string, unknown>) => r['note'], prop: (p: Props) => p.note },
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
describe('create always carries owner, null when undeclared', () => {
|
|
38
|
-
test('a create with no owner prop sends owner: null', async () => {
|
|
39
|
-
await withFake(async (fake) => {
|
|
40
|
-
const handlers = matchingHandlers(spec);
|
|
41
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
42
|
-
const post = fake.seen.find((s) => s.method === 'POST');
|
|
43
|
-
expect(post).toBeDefined();
|
|
44
|
-
expect(JSON.parse(post?.body ?? '{}')).toMatchObject({ name: 'Invoices', owner: null });
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('a create with an owner prop sends that owner', async () => {
|
|
49
|
-
await withFake(async (fake) => {
|
|
50
|
-
const handlers = matchingHandlers(spec);
|
|
51
|
-
await run(handlers.reconcile({ news: { name: 'Receipts', owner: 7 } }));
|
|
52
|
-
const post = fake.seen.find((s) => s.method === 'POST');
|
|
53
|
-
expect(JSON.parse(post?.body ?? '{}')).toMatchObject({ owner: 7 });
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('every request carries Accept: application/json; version=10', async () => {
|
|
59
|
-
await withFake(async (fake) => {
|
|
60
|
-
const handlers = matchingHandlers(spec);
|
|
61
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
62
|
-
expect(fake.seen.length).toBeGreaterThan(0);
|
|
63
|
-
for (const call of fake.seen) {
|
|
64
|
-
expect(call.headers.get('Accept')).toBe('application/json; version=10');
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('a no-drift reconcile makes zero writes', async () => {
|
|
70
|
-
await withFake(async (fake) => {
|
|
71
|
-
const handlers = matchingHandlers(spec);
|
|
72
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
73
|
-
const before = fake.seen.filter((s) => s.method !== 'GET').length;
|
|
74
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
75
|
-
const after = fake.seen.filter((s) => s.method !== 'GET').length;
|
|
76
|
-
expect(after).toBe(before);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('a 403 fails rather than reading as absent', async () => {
|
|
81
|
-
await withFake(async (fake) => {
|
|
82
|
-
const ops = matchingOperations(spec);
|
|
83
|
-
fake.forceStatus = 403;
|
|
84
|
-
const error = await runFailure(ops.read({ name: 'Invoices' }));
|
|
85
|
-
expect((error as { _tag?: string })._tag).toBe('PaperlessUnauthorized');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('a 404 on read folds to absent (create is the right plan)', async () => {
|
|
90
|
-
await withFake(async () => {
|
|
91
|
-
const ops = matchingOperations(spec);
|
|
92
|
-
expect(await run(ops.read({ name: 'Nothing here' }))).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('case-insensitive locate, case-sensitive identity', () => {
|
|
97
|
-
test('two differently-cased names never collapse into one object', async () => {
|
|
98
|
-
await withFake(async (fake) => {
|
|
99
|
-
const handlers = matchingHandlers(spec);
|
|
100
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
101
|
-
await run(handlers.reconcile({ news: { name: 'invoices' } }));
|
|
102
|
-
expect(fake.rows('tags').length).toBe(2);
|
|
103
|
-
|
|
104
|
-
const ops = matchingOperations(spec);
|
|
105
|
-
const exact = await run(ops.read({ name: 'Invoices' }));
|
|
106
|
-
expect(exact?.name).toBe('Invoices');
|
|
107
|
-
const other = await run(ops.read({ name: 'invoices' }));
|
|
108
|
-
expect(other?.name).toBe('invoices');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('read answers Unowned on a match, even one identical to the declaration (H1)', async () => {
|
|
114
|
-
await withFake(async () => {
|
|
115
|
-
const handlers = matchingHandlers(spec);
|
|
116
|
-
await run(handlers.reconcile({ news: { name: 'Invoices' } }));
|
|
117
|
-
const result = await run(handlers.read({ olds: { name: 'Invoices' } }));
|
|
118
|
-
expect(result).toBeDefined();
|
|
119
|
-
expect(Unowned.is(result)).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('read answers undefined, not Unowned, when nothing matches', async () => {
|
|
124
|
-
await withFake(async () => {
|
|
125
|
-
const handlers = matchingHandlers(spec);
|
|
126
|
-
expect(await run(handlers.read({ olds: { name: 'Nothing here' } }))).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
});
|