pi-lean-dimension 0.5.0 → 0.6.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 +5 -3
- package/node_modules/pi-lean-host/AGENTS.md +13 -6
- package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
- package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
- package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
- package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
- package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
- package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
- package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
- package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
- package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
- package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
- package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
- package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
- package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
- package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
- package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
- package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
- package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
- package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
- package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
- package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
- package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
- package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
- package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
- package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
- package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
- package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
- package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
- package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
- package/node_modules/pi-lean-host/core/auth.ts +2 -1
- package/node_modules/pi-lean-host/core/helpers.ts +29 -35
- package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
- package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
- package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
- package/node_modules/pi-lean-host/core/transport.ts +1 -1
- package/node_modules/pi-lean-host/package.json +1 -1
- package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
- package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
- package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
- package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
- package/node_modules/pi-lean-host/tools/utils.ts +69 -0
- package/node_modules/pi-lean-portal/AGENTS.md +4 -3
- package/node_modules/pi-lean-portal/README.md +9 -9
- package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
- package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
- package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
- package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
- package/node_modules/pi-lean-portal/browser-install.ts +454 -0
- package/node_modules/pi-lean-portal/browser-status.ts +21 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
- package/node_modules/pi-lean-portal/core/router.ts +7 -32
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
- package/node_modules/pi-lean-portal/index.ts +1 -18
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
- package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
- package/node_modules/pi-lean-search/AGENTS.md +5 -3
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
- package/node_modules/pi-lean-search/index.ts +0 -3
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +4 -4
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Covers:
|
|
5
5
|
* - Full write→verify→fix loop against local test server.
|
|
6
6
|
* - api-guide({}) catalog and api-guide({domain}) detail shapes.
|
|
7
|
-
* - api-learn validate-before-write (no half-write on invalid recipe).
|
|
8
7
|
* - api-fetch execute-fail message points at remediation paths.
|
|
9
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* (api-learn write-path tests live in api-learn-fetch-recipe.test.ts.)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
@@ -35,7 +35,6 @@ import { apiLearnTool } from "../tools/api-learn.js";
|
|
|
35
35
|
import { setStagingRoot } from "../core/staging.js";
|
|
36
36
|
import { setUserGuidesDir, invalidateCache } from "../core/guide-store.js";
|
|
37
37
|
import { writeSecret, setSecretsDir } from "../core/secrets-store.js";
|
|
38
|
-
import { parseApiGuide } from "../core/parse-api-guide.js";
|
|
39
38
|
import { selectGuideByShortName } from "../core/guide-catalog.js";
|
|
40
39
|
|
|
41
40
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -595,19 +594,6 @@ Op-collide guide.
|
|
|
595
594
|
`;
|
|
596
595
|
}
|
|
597
596
|
|
|
598
|
-
/** An invalid recipe (missing leading / in path). */
|
|
599
|
-
const INVALID_RECIPE = `---
|
|
600
|
-
schemaVersion: 1
|
|
601
|
-
domains: [example.com]
|
|
602
|
-
apiHost: https://api.example.com
|
|
603
|
-
operations:
|
|
604
|
-
- name: get
|
|
605
|
-
via: restGet
|
|
606
|
-
path: things/{id}
|
|
607
|
-
---
|
|
608
|
-
body
|
|
609
|
-
`;
|
|
610
|
-
|
|
611
597
|
// ═══════════════════════════════════════════════════════════════════
|
|
612
598
|
// Setup
|
|
613
599
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -691,11 +677,6 @@ function callLearn(
|
|
|
691
677
|
return apiLearnTool.execute("test", p, undefined, undefined, undefined as any);
|
|
692
678
|
}
|
|
693
679
|
|
|
694
|
-
/** Staged guide.md path for a domain (mirrors api-learn's staging). */
|
|
695
|
-
function stagedPath(domain: string): string {
|
|
696
|
-
return join(tmpStagingRoot, domain, "guide.md");
|
|
697
|
-
}
|
|
698
|
-
|
|
699
680
|
// ═══════════════════════════════════════════════════════════════════
|
|
700
681
|
// api-guide — catalog & detail
|
|
701
682
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -816,502 +797,6 @@ describe("api-guide", () => {
|
|
|
816
797
|
});
|
|
817
798
|
});
|
|
818
799
|
|
|
819
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
820
|
-
// api-learn — validate, write, no-half-write, example
|
|
821
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
822
|
-
|
|
823
|
-
describe("api-learn", () => {
|
|
824
|
-
it("prepends the authoring manual to template and fetch-recipe pulls", async () => {
|
|
825
|
-
// Template path ({domain, new: true}) — the manual travels with the
|
|
826
|
-
// staged draft.
|
|
827
|
-
const templateText = contentText(
|
|
828
|
-
await callLearn("example.com", undefined, { new: true }),
|
|
829
|
-
);
|
|
830
|
-
// Fetch-existing path ({domain}, no dir) — the manual travels
|
|
831
|
-
// with the staged raw recipe.
|
|
832
|
-
await callLearn("boe.es", boeRecipe(ctx.serverUrl));
|
|
833
|
-
invalidateCache();
|
|
834
|
-
const fetchText = contentText(await callLearn("boe.es"));
|
|
835
|
-
for (const text of [templateText, fetchText]) {
|
|
836
|
-
expect(text).toContain("authoring manual");
|
|
837
|
-
// Field reference + defaults + semantics stay.
|
|
838
|
-
expect(text).toContain("Required fields");
|
|
839
|
-
expect(text).toContain("a LIST of operation mappings");
|
|
840
|
-
expect(text).toContain("Key defaults");
|
|
841
|
-
expect(text).toContain("Executor semantics");
|
|
842
|
-
expect(text).toContain("joinUrl` strips a leading `/");
|
|
843
|
-
expect(text).toContain("pagination.base` seeds the page param");
|
|
844
|
-
expect(text).toContain("Page-size resolution (offset-limit/page)");
|
|
845
|
-
expect(text).toContain("→ omit (server default applies)");
|
|
846
|
-
expect(text).toContain("optional: true` on a ref");
|
|
847
|
-
// Guide-prose (agent-instructions) ability is taught, not lost.
|
|
848
|
-
expect(text).toContain("Guide prose");
|
|
849
|
-
expect(text).toContain("Guide notes");
|
|
850
|
-
// Points at the template entry point; no recipe body.
|
|
851
|
-
expect(text).toContain("new: true");
|
|
852
|
-
expect(text).not.toContain("searchDiary");
|
|
853
|
-
expect(text).not.toContain("```yaml");
|
|
854
|
-
}
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
// The template is a placeholder skeleton, not a worked example. It
|
|
858
|
-
// must fail closed (placeholder apiHost rejected) and carry no foreign API
|
|
859
|
-
// literals.
|
|
860
|
-
it("template is a placeholder skeleton that fails closed", async () => {
|
|
861
|
-
const text = contentText(
|
|
862
|
-
await callLearn("example.com", undefined, { new: true }),
|
|
863
|
-
);
|
|
864
|
-
// Result surfaces the staged path, not an inline yaml block.
|
|
865
|
-
expect(text).toContain(stagedPath("example.com"));
|
|
866
|
-
const template = readFileSync(stagedPath("example.com"), "utf-8");
|
|
867
|
-
expect(template).toContain("domains: [example.com]");
|
|
868
|
-
expect(template).toContain("<base url>");
|
|
869
|
-
expect(template).toContain("<short>");
|
|
870
|
-
expect(template).toContain("<emoji>");
|
|
871
|
-
expect(template).not.toMatch(
|
|
872
|
-
/apidatos|boe\.es|BOE|searchDiary|listConsolidada/,
|
|
873
|
-
);
|
|
874
|
-
// The prose-body (agent-instructions) ability is surfaced, not lost.
|
|
875
|
-
expect(template).toContain("agent-instruction prose");
|
|
876
|
-
expect(template).toContain("the closing ---");
|
|
877
|
-
// Fail-closed: the as-is template cannot save (placeholder apiHost
|
|
878
|
-
// is rejected by requireHttpUrl).
|
|
879
|
-
expect(parseApiGuide(template, { filename: "example.com" }).ok).toBe(false);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
it("validates and writes a valid recipe", async () => {
|
|
883
|
-
const text = contentText(await callLearn("boe.es", boeRecipe(ctx.serverUrl)));
|
|
884
|
-
expect(text).toContain("Guide saved");
|
|
885
|
-
expect(text).toContain("boe.es");
|
|
886
|
-
expect(text).toContain("searchDiary");
|
|
887
|
-
expect(text).toContain("api-fetch");
|
|
888
|
-
|
|
889
|
-
const filepath = join(tmpGuidesDir, "boe", "guide.md");
|
|
890
|
-
const content = readFileSync(filepath, "utf-8");
|
|
891
|
-
expect(content).toContain("apiHost:");
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// Companion — save summary echoes the resolved auth mapping (names only,
|
|
895
|
-
// never values): wrong-shape is loud, right-shape-but-wrong-name
|
|
896
|
-
// is eyeballable at save.
|
|
897
|
-
it("save summary names the auth header→secret mapping, never values", async () => {
|
|
898
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
899
|
-
invalidateCache();
|
|
900
|
-
const recipe = `---\nkind: api\ndomains: [authmap.example]\nshortName: AuthMap\napiHost: ${ctx.serverUrl}\nauth:\n kind: static-key\n secretRefs:\n Authorization:\n secret: apiKey\n prefix: "Bearer "\n X-Example-Pro-Key:\n secret: example_key\n prefix: ""\noperations:\n - name: get\n via: restGet\n path: /x\n accept: json\n---\n`;
|
|
901
|
-
const text = contentText(await callLearn("authmap.example", recipe));
|
|
902
|
-
expect(text).toContain("Auth: static-key");
|
|
903
|
-
expect(text).toContain("Authorization ← secret apiKey (Bearer )");
|
|
904
|
-
expect(text).toContain("X-Example-Pro-Key ← secret example_key");
|
|
905
|
-
// Empty prefix (bare-key header) renders without an empty paren.
|
|
906
|
-
expect(text).not.toContain("example_key ()");
|
|
907
|
-
// Names only — never the store values.
|
|
908
|
-
expect(text).not.toContain("s3cr3t");
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
it("rejects an invalid recipe without writing", async () => {
|
|
912
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
913
|
-
const text = contentText(await callLearn("broken", INVALID_RECIPE));
|
|
914
|
-
expect(text).toContain("Validation error");
|
|
915
|
-
expect(text).toContain("operations[0].path");
|
|
916
|
-
expect(text).toContain("NOT saved");
|
|
917
|
-
|
|
918
|
-
const filepath = join(tmpGuidesDir, "broken", "guide.md");
|
|
919
|
-
expect(() => readFileSync(filepath, "utf-8")).toThrow();
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// A validation failure names the failing field with expected/found and
|
|
923
|
-
// never writes. The manual-pointer tail is gone — the author already saw
|
|
924
|
-
// the manual on the pull that staged the draft.
|
|
925
|
-
it("reports validation failures with field/expected/found and does not save", async () => {
|
|
926
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
927
|
-
|
|
928
|
-
// Wrong-auth shape: name/secret fields instead of secretRefs/headerPrefixes.
|
|
929
|
-
const authText = contentText(
|
|
930
|
-
await callLearn(
|
|
931
|
-
"authbad.example",
|
|
932
|
-
`---
|
|
933
|
-
schemaVersion: 1
|
|
934
|
-
domains: [authbad.example]
|
|
935
|
-
apiHost: https://api.example.com
|
|
936
|
-
auth:
|
|
937
|
-
kind: static-key
|
|
938
|
-
name: X-EXAMPLE_PRO_API_KEY
|
|
939
|
-
secret: api_key
|
|
940
|
-
operations:
|
|
941
|
-
- name: get
|
|
942
|
-
via: restGet
|
|
943
|
-
path: /things
|
|
944
|
-
---
|
|
945
|
-
`,
|
|
946
|
-
),
|
|
947
|
-
);
|
|
948
|
-
expect(authText).toContain("auth.name");
|
|
949
|
-
expect(authText).toContain("NOT saved");
|
|
950
|
-
|
|
951
|
-
// Bad via.
|
|
952
|
-
const viaText = contentText(
|
|
953
|
-
await callLearn(
|
|
954
|
-
"viabad.example",
|
|
955
|
-
`---
|
|
956
|
-
schemaVersion: 1
|
|
957
|
-
domains: [viabad.example]
|
|
958
|
-
apiHost: https://api.example.com
|
|
959
|
-
operations:
|
|
960
|
-
- name: get
|
|
961
|
-
via: post
|
|
962
|
-
path: /things
|
|
963
|
-
---
|
|
964
|
-
`,
|
|
965
|
-
),
|
|
966
|
-
);
|
|
967
|
-
expect(viaText).toContain("operations[0].via");
|
|
968
|
-
expect(viaText).toContain("NOT saved");
|
|
969
|
-
|
|
970
|
-
// Unmapped field (frontmatter).
|
|
971
|
-
const fmText = contentText(await callLearn("fmbad.example", "just prose"));
|
|
972
|
-
expect(fmText).toContain("frontmatter");
|
|
973
|
-
expect(fmText).toContain("NOT saved");
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
it("returns a domain template when no recipe and no guide exists", async () => {
|
|
977
|
-
const text = contentText(await callLearn("somedomain.com"));
|
|
978
|
-
expect(text).toContain(stagedPath("somedomain.com"));
|
|
979
|
-
// The authoring manual travels with the staged template.
|
|
980
|
-
expect(text).toContain("authoring manual");
|
|
981
|
-
const draft = readFileSync(stagedPath("somedomain.com"), "utf-8");
|
|
982
|
-
expect(draft).toContain("domains: [somedomain.com]");
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
it("rejects a path-traversal domain without writing", async () => {
|
|
986
|
-
// Guards assertSafeDomain at the api-learn write boundary.
|
|
987
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
988
|
-
const result = await apiLearnTool.execute(
|
|
989
|
-
"test",
|
|
990
|
-
{
|
|
991
|
-
domain: "../../escape",
|
|
992
|
-
// assertSafeDomain rejects before this path is ever read.
|
|
993
|
-
dir: join(tmpStagingRoot, "escape"),
|
|
994
|
-
},
|
|
995
|
-
undefined,
|
|
996
|
-
undefined,
|
|
997
|
-
undefined as any,
|
|
998
|
-
);
|
|
999
|
-
const text = contentText(result);
|
|
1000
|
-
expect(text).toContain("Invalid domain");
|
|
1001
|
-
expect(result.details).toMatchObject({
|
|
1002
|
-
error: "invalid_domain",
|
|
1003
|
-
domain: "../../escape",
|
|
1004
|
-
});
|
|
1005
|
-
// Nothing written outside the guides dir.
|
|
1006
|
-
expect(() =>
|
|
1007
|
-
readFileSync(join(tmpGuidesDir, "..", "..", "escape", "guide.md"), "utf-8"),
|
|
1008
|
-
).toThrow();
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
it("rejects a description over 200 chars without writing", async () => {
|
|
1012
|
-
// Strict-on-write: the parser accepts any length (lenient-on-read),
|
|
1013
|
-
// but api-learn rejects >200 before writing.
|
|
1014
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1015
|
-
const longDesc = "x".repeat(201);
|
|
1016
|
-
const recipe = `---
|
|
1017
|
-
schemaVersion: 1
|
|
1018
|
-
kind: api
|
|
1019
|
-
domains: [toolong.example]
|
|
1020
|
-
description: ${longDesc}
|
|
1021
|
-
apiHost: ${ctx.serverUrl}
|
|
1022
|
-
operations:
|
|
1023
|
-
- name: get
|
|
1024
|
-
via: restGet
|
|
1025
|
-
path: /x
|
|
1026
|
-
accept: json
|
|
1027
|
-
---
|
|
1028
|
-
`;
|
|
1029
|
-
const result = await callLearn("toolong.example", recipe);
|
|
1030
|
-
const text = contentText(result);
|
|
1031
|
-
expect(text).toContain("NOT saved");
|
|
1032
|
-
expect(text).toContain("description");
|
|
1033
|
-
expect(text).toContain("201");
|
|
1034
|
-
expect(result.details).toMatchObject({ error: "description_too_long" });
|
|
1035
|
-
expect(() =>
|
|
1036
|
-
readFileSync(join(tmpGuidesDir, "toolong-example", "guide.md"), "utf-8"),
|
|
1037
|
-
).toThrow();
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
it("accepts a description at exactly 200 chars", async () => {
|
|
1041
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1042
|
-
const desc = "x".repeat(200);
|
|
1043
|
-
const recipe = `---
|
|
1044
|
-
schemaVersion: 1
|
|
1045
|
-
kind: api
|
|
1046
|
-
domains: [boundary.example]
|
|
1047
|
-
description: ${desc}
|
|
1048
|
-
apiHost: ${ctx.serverUrl}
|
|
1049
|
-
operations:
|
|
1050
|
-
- name: get
|
|
1051
|
-
via: restGet
|
|
1052
|
-
path: /x
|
|
1053
|
-
accept: json
|
|
1054
|
-
---
|
|
1055
|
-
`;
|
|
1056
|
-
const text = contentText(await callLearn("boundary.example", recipe));
|
|
1057
|
-
expect(text).toContain("Guide saved");
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
it("warns (does not reject) when domains collide with another guide", async () => {
|
|
1061
|
-
// Two guides, same `domains:` key, different directories. Valid — that's
|
|
1062
|
-
// the multi-recipe point. The write succeeds with a warning.
|
|
1063
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1064
|
-
invalidateCache();
|
|
1065
|
-
const first = `---
|
|
1066
|
-
schemaVersion: 1
|
|
1067
|
-
kind: api
|
|
1068
|
-
domains: [collide.example]
|
|
1069
|
-
organization: collide.org
|
|
1070
|
-
description: First surface.
|
|
1071
|
-
shortName: First
|
|
1072
|
-
apiHost: ${ctx.serverUrl}
|
|
1073
|
-
operations:
|
|
1074
|
-
- name: getFirst
|
|
1075
|
-
via: restGet
|
|
1076
|
-
path: /x
|
|
1077
|
-
accept: json
|
|
1078
|
-
---
|
|
1079
|
-
`;
|
|
1080
|
-
const second = `---
|
|
1081
|
-
schemaVersion: 1
|
|
1082
|
-
kind: api
|
|
1083
|
-
domains: [collide.example]
|
|
1084
|
-
organization: collide.org
|
|
1085
|
-
description: Second surface.
|
|
1086
|
-
shortName: Second
|
|
1087
|
-
apiHost: ${ctx.serverUrl}
|
|
1088
|
-
operations:
|
|
1089
|
-
- name: getSecond
|
|
1090
|
-
via: restGet
|
|
1091
|
-
path: /x
|
|
1092
|
-
accept: json
|
|
1093
|
-
---
|
|
1094
|
-
`;
|
|
1095
|
-
const firstText = contentText(await callLearn("collide-first", first));
|
|
1096
|
-
expect(firstText).toContain("Guide saved");
|
|
1097
|
-
expect(firstText).not.toContain("Multi-recipe");
|
|
1098
|
-
invalidateCache();
|
|
1099
|
-
const secondText = contentText(await callLearn("collide-second", second));
|
|
1100
|
-
expect(secondText).toContain("Guide saved");
|
|
1101
|
-
expect(secondText).toContain("Multi-recipe");
|
|
1102
|
-
// The collision warning renders the slug (slug("Second") = "second"),
|
|
1103
|
-
// not the `domain` arg "collide-second".
|
|
1104
|
-
expect(secondText).toContain("writing to directory `second`");
|
|
1105
|
-
expect(secondText).toContain("collide.example");
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
it("warns about a missing description when colliding", async () => {
|
|
1109
|
-
// When the second guide collides and omits description:, api-learn
|
|
1110
|
-
// recommends adding one (the primary disambiguation signal).
|
|
1111
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1112
|
-
invalidateCache();
|
|
1113
|
-
const first = `---
|
|
1114
|
-
schemaVersion: 1
|
|
1115
|
-
kind: api
|
|
1116
|
-
domains: [nodesc.example]
|
|
1117
|
-
organization: nodesc.org
|
|
1118
|
-
description: First surface.
|
|
1119
|
-
shortName: First
|
|
1120
|
-
apiHost: ${ctx.serverUrl}
|
|
1121
|
-
operations:
|
|
1122
|
-
- name: getFirst
|
|
1123
|
-
via: restGet
|
|
1124
|
-
path: /x
|
|
1125
|
-
accept: json
|
|
1126
|
-
---
|
|
1127
|
-
`;
|
|
1128
|
-
const second = `---
|
|
1129
|
-
schemaVersion: 1
|
|
1130
|
-
kind: api
|
|
1131
|
-
domains: [nodesc.example]
|
|
1132
|
-
organization: nodesc.org
|
|
1133
|
-
shortName: Second
|
|
1134
|
-
apiHost: ${ctx.serverUrl}
|
|
1135
|
-
operations:
|
|
1136
|
-
- name: getSecond
|
|
1137
|
-
via: restGet
|
|
1138
|
-
path: /x
|
|
1139
|
-
accept: json
|
|
1140
|
-
---
|
|
1141
|
-
`;
|
|
1142
|
-
await callLearn("nodesc-first", first);
|
|
1143
|
-
invalidateCache();
|
|
1144
|
-
const text = contentText(await callLearn("nodesc-second", second));
|
|
1145
|
-
expect(text).toContain("Guide saved");
|
|
1146
|
-
expect(text).toContain("Multi-recipe");
|
|
1147
|
-
expect(text).toContain("description");
|
|
1148
|
-
expect(text).toContain("recommended");
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
it("collision warning names /api delete as the recovery gesture", async () => {
|
|
1152
|
-
// The agent has no delete tool — when an existing guide is wrong, the
|
|
1153
|
-
// collision warning must point at the human-typed /api delete command,
|
|
1154
|
-
// naming the colliding directory (the one to remove).
|
|
1155
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1156
|
-
invalidateCache();
|
|
1157
|
-
const first = `---
|
|
1158
|
-
schemaVersion: 1
|
|
1159
|
-
kind: api
|
|
1160
|
-
domains: [recover.example]
|
|
1161
|
-
organization: recover.org
|
|
1162
|
-
shortName: First
|
|
1163
|
-
apiHost: ${ctx.serverUrl}
|
|
1164
|
-
operations:
|
|
1165
|
-
- name: getFirst
|
|
1166
|
-
via: restGet
|
|
1167
|
-
path: /x
|
|
1168
|
-
accept: json
|
|
1169
|
-
---
|
|
1170
|
-
`;
|
|
1171
|
-
const second = `---
|
|
1172
|
-
schemaVersion: 1
|
|
1173
|
-
kind: api
|
|
1174
|
-
domains: [recover.example]
|
|
1175
|
-
organization: recover.org
|
|
1176
|
-
shortName: Second
|
|
1177
|
-
apiHost: ${ctx.serverUrl}
|
|
1178
|
-
operations:
|
|
1179
|
-
- name: getSecond
|
|
1180
|
-
via: restGet
|
|
1181
|
-
path: /x
|
|
1182
|
-
accept: json
|
|
1183
|
-
---
|
|
1184
|
-
`;
|
|
1185
|
-
await callLearn("recover-first", first);
|
|
1186
|
-
invalidateCache();
|
|
1187
|
-
const text = contentText(await callLearn("recover-second", second));
|
|
1188
|
-
expect(text).toContain("Multi-recipe");
|
|
1189
|
-
// The existing guide's dirName is slug(shortName) = "first".
|
|
1190
|
-
expect(text).toContain("/api delete first");
|
|
1191
|
-
expect(text).toContain("the agent has no delete tool");
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
it("does not warn when updating the same guide's own directory", async () => {
|
|
1195
|
-
// Updating `foo.example` when `foo.example` already claims the domain is
|
|
1196
|
-
// not a collision — same dirName. No warning.
|
|
1197
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1198
|
-
invalidateCache();
|
|
1199
|
-
const r1 = `---
|
|
1200
|
-
schemaVersion: 1
|
|
1201
|
-
kind: api
|
|
1202
|
-
domains: [solo.example]
|
|
1203
|
-
shortName: Solo
|
|
1204
|
-
apiHost: ${ctx.serverUrl}
|
|
1205
|
-
operations:
|
|
1206
|
-
- name: get
|
|
1207
|
-
via: restGet
|
|
1208
|
-
path: /x
|
|
1209
|
-
accept: json
|
|
1210
|
-
---
|
|
1211
|
-
`;
|
|
1212
|
-
const r2 = r1.replace("name: get\n", "name: getMore\n");
|
|
1213
|
-
await callLearn("solo.example", r1);
|
|
1214
|
-
invalidateCache();
|
|
1215
|
-
const text = contentText(await callLearn("solo.example", r2));
|
|
1216
|
-
expect(text).toContain("Guide saved");
|
|
1217
|
-
expect(text).not.toContain("Multi-recipe");
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
// The template is the docs-side discoverability: no hardcoded
|
|
1221
|
-
// updated/verified dates (the tool stamps them when omitted) and a
|
|
1222
|
-
// static-key auth block to crib from.
|
|
1223
|
-
it("template has no hardcoded updated/verified dates", async () => {
|
|
1224
|
-
const text = contentText(
|
|
1225
|
-
await callLearn("example.com", undefined, { new: true }),
|
|
1226
|
-
);
|
|
1227
|
-
expect(text).toContain(stagedPath("example.com"));
|
|
1228
|
-
const example = readFileSync(stagedPath("example.com"), "utf-8");
|
|
1229
|
-
expect(example).not.toMatch(/^updated:/m);
|
|
1230
|
-
expect(example).not.toMatch(/^verified:/m);
|
|
1231
|
-
expect(example).toContain("stamped by the tool when omitted");
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
it("template documents the static-key auth block", async () => {
|
|
1235
|
-
const text = contentText(
|
|
1236
|
-
await callLearn("example.com", undefined, { new: true }),
|
|
1237
|
-
);
|
|
1238
|
-
expect(text).toContain(stagedPath("example.com"));
|
|
1239
|
-
const example = readFileSync(stagedPath("example.com"), "utf-8");
|
|
1240
|
-
expect(example).toContain("kind: static-key");
|
|
1241
|
-
expect(example).toContain("secret: <secret-name>");
|
|
1242
|
-
expect(example).toContain("secretRefs:");
|
|
1243
|
-
expect(example).toContain('prefix: "Bearer "');
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
// Write path — api-learn stamps schemaVersion on save.
|
|
1247
|
-
it("stamps schemaVersion on save when the recipe omits it", async () => {
|
|
1248
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1249
|
-
invalidateCache();
|
|
1250
|
-
const recipe = `---\nkind: api\ndomains: [stamp-absent.example]\nshortName: StampAbsent\napiHost: ${ctx.serverUrl}\noperations:\n - name: get\n via: restGet\n path: /x\n accept: json\n---\nProse body.\n`;
|
|
1251
|
-
await callLearn("stamp-absent.example", recipe);
|
|
1252
|
-
const raw = readFileSync(
|
|
1253
|
-
join(tmpGuidesDir, "stampabsent", "guide.md"),
|
|
1254
|
-
"utf-8",
|
|
1255
|
-
);
|
|
1256
|
-
expect(raw).toMatch(/^schemaVersion: 1$/m);
|
|
1257
|
-
// Prose body untouched.
|
|
1258
|
-
expect(raw).toContain("Prose body.");
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
it("replaces an explicit divergent schemaVersion on save", async () => {
|
|
1262
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1263
|
-
invalidateCache();
|
|
1264
|
-
const recipe = `---\nkind: api\nschemaVersion: 5\ndomains: [stamp-replace.example]\nshortName: StampReplace\napiHost: ${ctx.serverUrl}\noperations:\n - name: get\n via: restGet\n path: /x\n accept: json\n---\n`;
|
|
1265
|
-
await callLearn("stamp-replace.example", recipe);
|
|
1266
|
-
const raw = readFileSync(
|
|
1267
|
-
join(tmpGuidesDir, "stampreplace", "guide.md"),
|
|
1268
|
-
"utf-8",
|
|
1269
|
-
);
|
|
1270
|
-
expect(raw).toMatch(/^schemaVersion: 1$/m);
|
|
1271
|
-
expect(raw).not.toMatch(/^schemaVersion: 5$/m);
|
|
1272
|
-
});
|
|
1273
|
-
|
|
1274
|
-
it("never touches a schemaVersion string in the prose body", async () => {
|
|
1275
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1276
|
-
invalidateCache();
|
|
1277
|
-
const recipe = `---\nkind: api\ndomains: [stamp-prose.example]\nshortName: StampProse\napiHost: ${ctx.serverUrl}\noperations:\n - name: get\n via: restGet\n path: /x\n accept: json\n---\nThe schemaVersion: 5 in this prose must stay untouched.\n`;
|
|
1278
|
-
await callLearn("stamp-prose.example", recipe);
|
|
1279
|
-
const raw = readFileSync(
|
|
1280
|
-
join(tmpGuidesDir, "stampprose", "guide.md"),
|
|
1281
|
-
"utf-8",
|
|
1282
|
-
);
|
|
1283
|
-
// Frontmatter got the stamp...
|
|
1284
|
-
expect(raw).toMatch(/^schemaVersion: 1$/m);
|
|
1285
|
-
// ...and the prose line is untouched (still schemaVersion: 5).
|
|
1286
|
-
expect(raw).toContain(
|
|
1287
|
-
"The schemaVersion: 5 in this prose must stay untouched.",
|
|
1288
|
-
);
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
it("preserves comments and key order when stamping", async () => {
|
|
1292
|
-
setUserGuidesDir(tmpGuidesDir);
|
|
1293
|
-
invalidateCache();
|
|
1294
|
-
const recipe = `---\nkind: api\ndomains: [stamp-order.example]\n# a comment that must survive\nshortName: StampOrder\napiHost: ${ctx.serverUrl}\noperations:\n - name: get\n via: restGet\n path: /x\n accept: json\n---\n`;
|
|
1295
|
-
await callLearn("stamp-order.example", recipe);
|
|
1296
|
-
const raw = readFileSync(
|
|
1297
|
-
join(tmpGuidesDir, "stamporder", "guide.md"),
|
|
1298
|
-
"utf-8",
|
|
1299
|
-
);
|
|
1300
|
-
expect(raw).toContain("# a comment that must survive");
|
|
1301
|
-
// Key order preserved; schemaVersion inserted after operations, before
|
|
1302
|
-
// the closing --- (no YAML round-trip).
|
|
1303
|
-
const idxDomains = raw.indexOf("domains:");
|
|
1304
|
-
const idxShort = raw.indexOf("shortName:");
|
|
1305
|
-
const idxApi = raw.indexOf("apiHost:");
|
|
1306
|
-
const idxOps = raw.indexOf("operations:");
|
|
1307
|
-
const idxSV = raw.indexOf("schemaVersion: 1");
|
|
1308
|
-
expect(idxDomains).toBeLessThan(idxShort);
|
|
1309
|
-
expect(idxShort).toBeLessThan(idxApi);
|
|
1310
|
-
expect(idxApi).toBeLessThan(idxOps);
|
|
1311
|
-
expect(idxOps).toBeLessThan(idxSV);
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
800
|
/** A recipe for the large-response endpoint (spill truncation). */
|
|
1316
801
|
function largeResponseRecipe(apiHost: string): string {
|
|
1317
802
|
return `---
|
|
@@ -1597,7 +1082,6 @@ describe("api-fetch", () => {
|
|
|
1597
1082
|
parsed = JSON.parse(fileContent);
|
|
1598
1083
|
} catch {
|
|
1599
1084
|
expect.fail("Spill file contains invalid JSON");
|
|
1600
|
-
return;
|
|
1601
1085
|
}
|
|
1602
1086
|
expect(Array.isArray((parsed as Record<string, unknown>).results)).toBe(true);
|
|
1603
1087
|
expect(
|