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.
Files changed (104) hide show
  1. package/README.md +5 -3
  2. package/node_modules/pi-lean-host/AGENTS.md +13 -6
  3. package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
  4. package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
  5. package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
  6. package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
  7. package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
  8. package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
  9. package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
  10. package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
  11. package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
  12. package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
  13. package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
  14. package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
  15. package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
  16. package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
  17. package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
  18. package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
  19. package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
  20. package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
  21. package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
  22. package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
  23. package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
  24. package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
  25. package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
  26. package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
  27. package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
  28. package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
  29. package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
  30. package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
  31. package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
  32. package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
  33. package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
  34. package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
  35. package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
  36. package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
  37. package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
  38. package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
  39. package/node_modules/pi-lean-host/core/auth.ts +2 -1
  40. package/node_modules/pi-lean-host/core/helpers.ts +29 -35
  41. package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
  42. package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
  43. package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
  44. package/node_modules/pi-lean-host/core/transport.ts +1 -1
  45. package/node_modules/pi-lean-host/package.json +1 -1
  46. package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
  47. package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
  48. package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
  49. package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
  50. package/node_modules/pi-lean-host/tools/utils.ts +69 -0
  51. package/node_modules/pi-lean-portal/AGENTS.md +4 -3
  52. package/node_modules/pi-lean-portal/README.md +9 -9
  53. package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
  54. package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
  55. package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
  56. package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
  57. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
  58. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
  59. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
  60. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  61. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  62. package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
  63. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
  64. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
  65. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
  66. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
  67. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
  68. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
  69. package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
  70. package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
  71. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  72. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
  77. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
  84. package/node_modules/pi-lean-portal/browser-install.ts +454 -0
  85. package/node_modules/pi-lean-portal/browser-status.ts +21 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
  87. package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
  88. package/node_modules/pi-lean-portal/core/router.ts +7 -32
  89. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
  90. package/node_modules/pi-lean-portal/index.ts +1 -18
  91. package/node_modules/pi-lean-portal/package.json +2 -1
  92. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
  93. package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
  94. package/node_modules/pi-lean-search/AGENTS.md +5 -3
  95. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
  96. package/node_modules/pi-lean-search/index.ts +0 -3
  97. package/node_modules/pi-lean-search/package.json +1 -1
  98. package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
  99. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  100. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  101. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  102. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  103. package/node_modules/yaml/package.json +1 -1
  104. 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
- * - api-learn prepends the authoring manual to template/fetch-recipe pulls.
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(