Compare commits

..

69 Commits

Author SHA1 Message Date
Aiden Cline 314388f46e fix(codemode): align promise combinators with Test262 2026-07-08 17:51:41 -05:00
Aiden Cline 6273304f2a Merge remote-tracking branch 'origin/v2' into promise-chaining
# Conflicts:
#	packages/codemode/src/interpreter/runtime.ts
#	packages/codemode/src/tool-runtime.ts
2026-07-08 17:33:50 -05:00
Aiden Cline ec8eea7cbe test(codemode): add Test262 array coverage (#35965) 2026-07-08 17:26:11 -05:00
Aiden Cline b44a981eef feat(core): global form support (#35959) 2026-07-08 17:25:34 -05:00
James Long 79415f625a feat(tui): revamp session export (#35971) 2026-07-08 18:14:43 -04:00
Simon Klee 19f42f7102 feat(v2/cli): add console login (#35969) 2026-07-09 00:00:09 +02:00
Aiden Cline 212c9f99ee test(codemode): add Test262 string coverage (#35940) 2026-07-08 15:51:54 -05:00
James Long 99e52303e6 feat(core): discover ecosystem skill directories (#35956) 2026-07-08 16:34:41 -04:00
Dax Raad 88925ec6e6 fix(tui): allow callers without log sink 2026-07-08 16:00:52 -04:00
Dax Raad cedf365674 feat(tui): add structured logging 2026-07-08 16:00:07 -04:00
Kit Langton 5b39972947 fix(core): await initial plugin readiness (#35755) 2026-07-08 15:12:45 -04:00
James Long 09553d46e0 refactor(simulation): remove filesystem layers (#35938) 2026-07-08 12:22:04 -04:00
James Long e1d72fa338 fix(simulation): stabilize screenshot artifacts (#35933) 2026-07-08 11:48:53 -04:00
Dax Raad b3cf749a7f fix(tui): show resolved skill name 2026-07-08 11:46:02 -04:00
Dax Raad dd842f9862 fix(cli): elect one service process 2026-07-08 11:46:02 -04:00
Kit Langton 3dd29094f4 feat(http-recorder): sync recorder v0.3 (#35619) 2026-07-08 11:41:59 -04:00
Aiden Cline 05e8d5be73 fix(core): preserve prompt attachment order (#35921) 2026-07-08 10:06:58 -05:00
Dax Raad 8b634e4a58 refactor(tui): simplify client data state 2026-07-08 10:37:48 -04:00
James Long c6156f171c feat(simulation): stream UI recordings (#35909) 2026-07-08 10:33:11 -04:00
Dax Raad 4d2b06f8cf docs: prohibit bypassing git hooks 2026-07-08 10:11:46 -04:00
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 bf15c97e4b Revert "fix(tui): paginate session history"
This reverts commit cc29f86cdc.
2026-07-08 13:45:20 +00:00
Shoubhit Dash 3cbcd8d727 feat(llm): complete provider package entrypoints (#35907) 2026-07-08 18:41:46 +05:30
Dax Raad cc29f86cdc fix(tui): paginate session history 2026-07-08 01:34:43 -04:00
Dax Raad 86e3828da6 fix(core): watch location only when vcs is present 2026-07-08 00:38:56 -04:00
James Long 8d00a51766 feat(simulation): support multiple tool calls (#35861) 2026-07-08 00:35:18 -04:00
opencode-agent[bot] ed4f833813 fix(core): preserve provider metadata namespaces (#35817)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-07-07 21:45:58 -05:00
opencode-agent[bot] e96c24ce2e test(core): fix mutable plugin fixture (#35837) 2026-07-07 22:22:12 -04:00
Kit Langton ed6ad272ec refactor(schema): apply session review decisions (#35793) 2026-07-07 22:10:11 -04:00
Kit Langton d27746e5b3 test(core): simplify runner test fixtures (#35832) 2026-07-07 21:58:52 -04:00
Kit Langton eb661e6e6d fix(core): settle unadvertised tool calls and repair session hook test contexts 2026-07-07 21:51:23 -04:00
Dax Raad 3379e44abb refactor(core): rename ApplyPatchTool to PatchTool 2026-07-07 21:16:00 -04:00
Dax e1abf59c59 fix(core): provide PluginHooks in plugin test layer (#35811) 2026-07-07 20:03:41 -04:00
Dax 4f976bcf1a feat(plugin): add session request hook (#35794) 2026-07-07 19:56:47 -04:00
Kit Langton e25a724bc2 test(tui): cover patch diff display aliases (#35810) 2026-07-07 19:44:54 -04:00
Kit Langton 521ea87192 fix(core): retain reasoning delta state (#35758) 2026-07-07 19:44:40 -04:00
Aiden Cline 3100701488 fix(tui): restore prompt context usage (#35795) 2026-07-07 17:49:33 -05:00
Kit Langton 9c54291ae8 fix(tui): restore forms on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:47:13 +00:00
Dax Raad 52ad916ba4 fix(tui): restore permissions on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:44:26 +00:00
Dax Raad 5db320b02d refactor: rename apply_patch tool 2026-07-07 16:27:25 -04:00
Aiden Cline 910e37f6d8 fix: update v2 session usage metrics (#35468) 2026-07-07 14:31:54 -05:00
Kit Langton 81f6e06681 feat: enable background subagents by default 2026-07-07 15:18:46 -04:00
James Long da68e2865e feat(tui): wire move session command (#35203) 2026-07-07 14:49:11 -04:00
Aiden Cline f86e1cc1ff fix(codemode): preserve collection value identity (#35776) 2026-07-07 13:40:55 -05:00
Aiden Cline 947bbf9490 feat(mcp): expose resource catalog API (#35773) 2026-07-07 13:34:29 -05:00
James Long 3e57b43a4a feat(session): support admit-only synthetic messages (#35774) 2026-07-07 14:17:20 -04:00
opencode-agent[bot] cb18a42a47 docs(codemode): clarify implicit return guidance (#35763)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 12:48:05 -05:00
Aiden Cline 1fb2837fd3 feat(codemode): expand numeric standard library (#35749) 2026-07-07 12:11:57 -05:00
Aiden Cline 66a8d830aa feat(core): support MCP resources (#35656) 2026-07-07 11:55:06 -05:00
opencode-agent[bot] d0733f83bf fix(codemode): return final top-level expression (#35759)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 11:44:50 -05:00
Kit Langton b2ce195bda fix(core): normalize Location.Ref keys in LocationServiceMap (#35757) 2026-07-07 12:25:29 -04:00
James Long 6c065ca6a5 feat(simulation): render driven UI screenshots and videos (#35739) 2026-07-07 12:15:01 -04:00
Dax Raad dd1c007877 docs 2026-07-07 12:08:45 -04:00
Kit Langton 8deb0e5780 fix(core): preserve SDK plugins across location eviction (#35725) 2026-07-07 11:43:40 -04:00
Aiden Cline f488089179 fix: preserve compaction files and classify Z.AI overflow (#35747)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
2026-07-07 09:54:31 -05:00
Kit Langton e3a39b214f fix(core): lower failed reasoning to text (#35735) 2026-07-07 09:49:17 -04:00
James Long b6a2912bb1 refactor(simulation): type RPC request payloads (#35734) 2026-07-07 09:21:19 -04:00
Aiden Cline 34b4fa9543 chore: merge v2 2026-07-06 23:04:26 -05:00
Aiden Cline 99c4e201d5 chore: retry ci 2026-07-07 00:41:35 +00:00
Aiden Cline 61391353c9 fix(app): add session deletion event id 2026-07-06 23:52:20 +00:00
Aiden Cline 0a0d7d69cd fix(codemode): preserve promise reaction order 2026-07-06 18:29:59 -05:00
Aiden Cline d7d0113591 docs(codemode): narrow chaining guidance 2026-07-06 18:20:32 -05:00
Aiden Cline c9b4eddf25 docs(codemode): clarify promise status 2026-07-06 18:18:19 -05:00
Aiden Cline 78520af164 fix(codemode): supervise promise lifecycles 2026-07-06 18:11:16 -05:00
Aiden Cline 9445497fc5 docs(codemode): track promise status 2026-07-06 17:50:21 -05:00
Aiden Cline cf5e278447 fix(codemode): return promises from combinators 2026-07-06 17:32:58 -05:00
Aiden Cline c3b9d03bdb fix(codemode): preserve promise race losers 2026-07-06 17:24:00 -05:00
Aiden Cline 0800770b0d docs(codemode): track thenable assimilation 2026-07-06 17:04:03 -05:00
Aiden Cline 8b53f362e8 docs(codemode): describe promise chaining 2026-07-06 16:58:55 -05:00
Aiden Cline 1289e00778 feat(codemode): support promise chaining 2026-07-06 16:52:00 -05:00
487 changed files with 23843 additions and 13469 deletions
+2
View File
@@ -19,6 +19,8 @@ Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes a
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
Never bypass Git hooks. Do not use `--no-verify` or otherwise disable, skip, or circumvent commit or push hooks. If a hook fails, fix the failure or stop and report it to the user.
## Style Guide
### General Principles
+122 -8
View File
@@ -101,7 +101,6 @@
"dependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -115,6 +114,7 @@
"effect": "catalog:",
"fuzzysort": "catalog:",
"jsonc-parser": "3.3.1",
"open": "10.1.2",
"opentui-spinner": "catalog:",
"semver": "catalog:",
"solid-js": "catalog:",
@@ -527,10 +527,10 @@
"name": "@opencode-ai/http-recorder",
"version": "1.17.14",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
@@ -539,7 +539,7 @@
"typescript": "catalog:",
},
"peerDependencies": {
"effect": "4.0.0-beta.83",
"effect": "catalog:",
},
},
"packages/httpapi-codegen": {
@@ -874,6 +874,8 @@
"name": "@opencode-ai/simulation",
"version": "1.17.13",
"dependencies": {
"@fontsource/adwaita-mono": "5.2.1",
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opentui/core": "catalog:",
@@ -1612,11 +1614,11 @@
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
@@ -1724,6 +1726,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@fontsource/adwaita-mono": ["@fontsource/adwaita-mono@5.2.1", "", {}, "sha512-6+Q1UIvklJ9REijs6kv7YlRNt6yktRj0iW8H69YIugdD9P2h3eIX1AB8/9ICMfpVyVeywlsrCXg82y/LfRrjyg=="],
"@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="],
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
@@ -1984,6 +1988,30 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@napi-rs/canvas": ["@napi-rs/canvas@1.0.2", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.2", "@napi-rs/canvas-darwin-arm64": "1.0.2", "@napi-rs/canvas-darwin-x64": "1.0.2", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", "@napi-rs/canvas-linux-arm64-musl": "1.0.2", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-musl": "1.0.2", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", "@napi-rs/canvas-win32-x64-msvc": "1.0.2" } }, "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ=="],
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g=="],
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ=="],
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A=="],
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ=="],
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA=="],
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA=="],
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw=="],
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g=="],
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q=="],
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ=="],
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
@@ -2278,7 +2306,7 @@
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="],
@@ -2570,7 +2598,37 @@
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
@@ -3112,6 +3170,8 @@
"@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
@@ -3184,6 +3244,12 @@
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="],
"@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="],
"@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
@@ -4988,6 +5054,8 @@
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="],
@@ -5402,6 +5470,8 @@
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="],
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
@@ -6022,8 +6092,12 @@
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -6620,6 +6694,10 @@
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -6640,6 +6718,8 @@
"@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
@@ -6748,6 +6828,14 @@
"@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="],
"@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="],
"@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
@@ -7044,6 +7132,8 @@
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -7708,6 +7798,10 @@
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
@@ -7716,6 +7810,8 @@
"@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
"@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
@@ -7786,6 +7882,22 @@
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
@@ -8352,6 +8464,8 @@
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -20,7 +20,7 @@ const profiles = [
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
{
name: "multi patch",
tool: "apply_patch",
tool: "patch",
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
},
] as const
@@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
userMessage(),
assistantMessage(
[
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
textPart(followingID, "Following incremental patch"),
],
{ completed: false },
@@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated(
toolPart(
patchID,
"apply_patch",
"patch",
"running",
{ files: [first.filePath, second.filePath] },
{ metadata: { files: [first, second] } },
@@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated(
toolPart(
patchID,
"apply_patch",
"patch",
"completed",
{ files: [first.filePath, second.filePath, third.filePath] },
{ metadata: { files: [first, second, third] } },
@@ -295,7 +295,7 @@ function performanceTurn(index: number) {
messageID: assistantID,
type: "tool",
callID: `call_0000_${suffix}_patch`,
tool: "apply_patch",
tool: "patch",
state: {
status: "completed",
input: { patchText: realisticPatch(index) },
@@ -131,7 +131,7 @@ function toolPart(
): MessagePart {
const metadata =
metadataOverride ??
(tool === "apply_patch"
(tool === "patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write"
? {
@@ -219,7 +219,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []),
...(index % 8 === 0
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []),
...(index % 7 === 0
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
@@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
assistantMessage([
toolPart(
id,
"apply_patch",
"patch",
"completed",
{ files: ["src/a.ts"] },
{
@@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
assistantMessage([
toolPart(
patchID,
"apply_patch",
"patch",
"completed",
{ files: files.map((file) => file.filePath) },
{ metadata: { files } },
@@ -246,7 +246,7 @@ function editPart(id: string) {
function patchPart(id: string) {
return toolPart(
id,
"apply_patch",
"patch",
"completed",
{ files: ["src/a.ts", "src/b.ts"] },
{
@@ -8,7 +8,7 @@ import {
} from "../performance/timeline-stability/fixture"
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const parts = ordinary.map((tool, index) =>
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
)
@@ -90,7 +90,7 @@ function questionInput() {
function errorInput(tool: string) {
if (tool === "bash") return { command: "exit 1" }
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
if (tool === "apply_patch") return { files: ["src/error.ts"] }
if (tool === "patch") return { files: ["src/error.ts"] }
if (tool === "webfetch") return { url: "https://example.com" }
if (tool === "websearch") return { query: "failure" }
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
@@ -120,7 +120,7 @@ function toolPart(
outputLength = 160,
): MessagePart {
const metadata =
tool === "apply_patch"
tool === "patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write"
? {
@@ -199,7 +199,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []),
...(index % 8 === 0
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []),
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
+1 -1
View File
@@ -55,7 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const path = url.pathname
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
if (path === "/global/health") return json(route, { healthy: true })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
if (path === "/permission")
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
if (path === "/question")
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
@@ -188,7 +188,7 @@ export function applyDirectoryEvent(input: {
break
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break
}
@@ -5,7 +5,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@@ -33,7 +33,7 @@ describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -67,7 +67,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -4,7 +4,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client"
@@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -13,7 +13,7 @@ import type {
ReferenceInfo,
Session,
SessionStatus,
SnapshotFileDiff,
FileDiffInfo,
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
@@ -51,7 +51,7 @@ export type State = {
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: SnapshotFileDiff[]
[sessionID: string]: FileDiffInfo[]
}
todo: {
[sessionID: string]: Todo[]
+3 -3
View File
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
FileDiffInfo,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { batch } from "solid-js"
@@ -139,7 +139,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>,
session_diff: {} as Record<string, SnapshotFileDiff[]>,
session_diff: {} as Record<string, FileDiffInfo[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>,
@@ -769,7 +769,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
return
}
@@ -1,6 +1,6 @@
import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import type {
SessionReviewCommentActions,
@@ -14,7 +14,7 @@ import type { LineComment } from "@/context/comments"
export type DiffStyle = "unified" | "split"
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
type ReviewDiff = FileDiffInfo | VcsFileDiff
export interface SessionReviewTabProps {
title?: JSX.Element
@@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -23,7 +23,6 @@ import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
import {
@@ -36,15 +35,9 @@ import {
import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
diffs: () => (FileDiffInfo | VcsFileDiff)[]
diffsReady: () => boolean
empty: () => string
hasReview: () => boolean
@@ -59,7 +52,6 @@ export function SessionSidePanel(props: {
}) {
const layout = useLayout()
const settings = useSettings()
const sync = useSync()
const file = useFile()
const language = useLanguage()
const command = useCommand()
@@ -88,7 +80,7 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffs = createMemo(() => props.diffs())
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
@@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
const value = row()
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool)
return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
}
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
let contentMeasureFrame: number | undefined
@@ -1,17 +1,13 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Kind } from "@/components/file-tree-v2"
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
export type RenderDiff = FileDiffInfo | VcsFileDiff
export function normalizePath(p: string) {
return normalizeFileTreeV2Path(p)
}
export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function reviewDiffKinds(diffs: RenderDiff[]) {
const merge = (a: Kind | undefined, b: Kind) => {
if (!a) return b
@@ -1,5 +1,5 @@
import { createMemo, createSignal, Show, type JSX } from "solid-js"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
@@ -21,16 +21,11 @@ import type {
import FileTreeV2 from "@/components/file-tree-v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import {
filterRenderableDiff,
filterReviewFiles,
reviewDiffKinds,
type RenderDiff,
} from "@/pages/session/v2/review-diff-kinds"
import { filterReviewFiles, reviewDiffKinds, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
type ReviewDiff = FileDiffInfo | VcsFileDiff
export type ReviewPanelV2Props = {
title?: JSX.Element
@@ -54,7 +49,7 @@ export type ReviewPanelV2Props = {
export function ReviewPanelV2(props: ReviewPanelV2Props) {
const sdk = useSDK()
const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff))
const diffs = createMemo(() => props.diffs())
const filteredFiles = createMemo(() =>
filterReviewFiles(
diffs().map((diff) => diff.file),
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
@@ -9,7 +9,7 @@ const item = {
additions: 1,
deletions: 1,
status: "modified",
} satisfies SnapshotFileDiff
} satisfies FileDiffInfo
describe("diffs", () => {
test("keeps valid arrays", () => {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
type Diff = SnapshotFileDiff | VcsFileDiff
type Diff = FileDiffInfo
function diff(value: unknown): value is Diff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
Regular → Executable
View File
+1 -1
View File
@@ -33,7 +33,6 @@
"dependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -47,6 +46,7 @@
"effect": "catalog:",
"fuzzysort": "catalog:",
"jsonc-parser": "3.3.1",
"open": "10.1.2",
"opentui-spinner": "catalog:",
"semver": "catalog:",
"solid-js": "catalog:",
-66
View File
@@ -1,66 +0,0 @@
export * as CodeModeHost from "./code-mode"
import { NodeHttpClient } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { OpenAPI, Tool } from "@opencode-ai/codemode"
import { Api } from "@opencode-ai/server/api"
import { ServerAuth } from "@opencode-ai/server/auth"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi"
import type { Server } from "node:http"
export function replacements(server: Server, password: string): LayerNode.Replacements {
return [ToolRegistry.codeModeReplacement(makeTools(client(server), password))]
}
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
return {
opencode: bindTools(
OpenAPI.fromSpec({
spec: { ...OpenApi.fromApi(Api) },
baseUrl: "http://opencode.local",
headers: ServerAuth.headers({ username: "opencode", password }),
}).tools,
client,
),
}
}
function client(server: Server) {
return Layer.effect(
HttpClient.HttpClient,
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
return HttpClient.mapRequest(client, (request) => {
const address = server.address()
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
const local =
address.address === "0.0.0.0" ? "127.0.0.1" : address.address === "::" ? "::1" : address.address
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
const url = new URL(request.url)
return HttpClientRequest.setUrl(
request,
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
)
})
}),
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
}
function bindTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
return Object.fromEntries(
Object.entries(tools).map(([name, value]) => [
name,
Tool.isDefinition<HttpClient.HttpClient>(value)
? Tool.make({
description: value.description,
input: value.input,
output: value.output,
run: (input) => value.run(input).pipe(Effect.provide(client)),
})
: bindTools(value, client),
]),
)
}
+11
View File
@@ -75,6 +75,17 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
description: "Debugging and troubleshooting tools",
commands: [Spec.make("agents", { description: "List all agents" })],
}),
Spec.make("console", {
description: "Manage OpenCode Console access",
commands: [
Spec.make("login", {
description: "Log in to OpenCode Console",
params: {
url: Argument.string("url").pipe(Argument.withDescription("Console server URL"), Argument.optional),
},
}),
],
}),
Spec.make("mcp", {
description: "Manage MCP (Model Context Protocol) servers",
commands: [
@@ -0,0 +1,116 @@
import { Cause, Effect, Exit, Option } from "effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { AppProcess } from "@opencode-ai/core/process"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Daemon } from "../../../daemon"
import { createTimelineHost, type TimelineHost } from "../../../ui/timeline"
const integrationID = "opencode"
const location = { directory: process.cwd() }
export default Runtime.handler(
Commands.commands.console.commands.login,
Effect.fn("cli.console.login")(function* (input) {
const timeline = yield* Effect.acquireRelease(
Effect.promise(() => createTimelineHost()),
(value) => request(() => value.close()).pipe(Effect.ignore),
)
const exit = yield* login(timeline, Option.getOrUndefined(input.url)).pipe(
Effect.raceFirst(AppProcess.waitForAbort(timeline.signal)),
Effect.exit,
)
if (Exit.isSuccess(exit)) return
const cancelled = timeline.signal.aborted
yield* request(() => timeline.failure(cancelled ? "Authorization cancelled" : errorMessage(exit.cause))).pipe(
Effect.ignore,
)
process.exitCode = cancelled ? 130 : 1
}),
)
const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHost, server?: string) {
yield* request(() => timeline.intro("Log in"))
yield* request(() => timeline.pending("Connecting to OpenCode..."))
const transport = yield* Daemon.transport({ mode: "shared" })
const client = OpenCode.make({ baseUrl: transport.url, headers: transport.headers })
const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal }))
const integration = yield* required(found.data, "OpenCode Console integration is unavailable")
const method = yield* required(
integration.methods.find((candidate) => candidate.type === "oauth"),
"OpenCode Console login is unavailable",
)
yield* request(() => timeline.pending("Starting authorization..."))
const started = yield* request((signal) =>
client.integration.connect.oauth(
{
integrationID,
methodID: method.id,
inputs: server ? { server } : {},
location,
},
{ signal },
),
)
const attempt = started.data
yield* Effect.addFinalizer(() =>
request(() =>
client.integration.attempt.cancel(
{ attemptID: attempt.attemptID, location },
{ signal: AbortSignal.timeout(5_000) },
),
).pipe(Effect.ignore),
)
if (attempt.mode !== "auto") yield* Effect.fail(new Error("OpenCode Console requires a device login"))
yield* request(() => timeline.item(`Go to: ${attempt.url}`))
yield* request(() => timeline.item(attempt.instructions))
yield* request(async () => {
const { default: open } = await import("open")
await open(attempt.url)
}).pipe(Effect.ignore)
yield* request(() => timeline.pending("Waiting for authorization..."))
const status = yield* waitForConsoleLogin(client, attempt.attemptID)
if (status.status === "failed") yield* Effect.fail(new Error(status.message))
if (status.status === "expired") yield* Effect.fail(new Error("Device code expired"))
yield* request(() => timeline.success("Connected to OpenCode Console"))
yield* request(() => timeline.outro("Done"))
})
const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* (
client: OpenCodeClient,
attemptID: string,
) {
while (true) {
const response = yield* request((signal) =>
client.integration.attempt.status({ attemptID, location }, { signal }),
)
if (response.data.status !== "pending") return response.data
yield* Effect.sleep(500)
}
})
function request<A>(task: (signal: AbortSignal) => Promise<A>) {
return Effect.tryPromise({
try: task,
catch: (cause) => cause,
})
}
function required<A>(value: A | null | undefined, message: string) {
return value === null || value === undefined ? Effect.fail(new Error(message)) : Effect.succeed(value)
}
function errorMessage(cause: Cause.Cause<unknown>) {
const error = Cause.squash(cause)
if (error instanceof Error) return error.message
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
return error.message
}
return String(error)
}
+3
View File
@@ -24,6 +24,9 @@ const Handlers = Runtime.handlers(Commands, {
debug: {
agents: () => import("./commands/handlers/debug/agents"),
},
console: {
login: () => import("./commands/handlers/console/login"),
},
mcp: {
list: () => import("./commands/handlers/mcp/list"),
add: () => import("./commands/handlers/mcp/add"),
+3 -2
View File
@@ -37,7 +37,8 @@ function defaultCost(model: CurrentModel) {
export function runAgent(input: CurrentAgent): RunAgent {
return {
name: input.id,
id: input.id,
name: input.name,
description: input.description,
mode: input.mode,
hidden: input.hidden,
@@ -53,7 +54,7 @@ export function runCommand(input: CurrentCommand): RunCommand {
export function runSkill(input: CurrentSkill): RunCommand {
return {
name: input.name,
name: input.id,
description: input.description,
source: "skill",
}
+2 -2
View File
@@ -628,11 +628,11 @@ function emitEdit(state: State): void {
function emitPatch(state: State): void {
const file = path.join(process.cwd(), "src", "demo-format.ts")
const ref = make(state, "apply_patch", {
const ref = make(state, "patch", {
patchText: "*** Begin Patch\n*** End Patch",
})
doneTool(state, ref, {
title: "apply_patch",
title: "patch",
output: "",
metadata: {
files: [
+2 -2
View File
@@ -333,10 +333,10 @@ export function createPromptState(input: PromptInput): PromptState {
.map((item) => ({
kind: "mention",
display: "@" + item.name,
value: item.name,
value: item.id,
part: {
type: "agent",
name: item.name,
name: item.id,
source: {
start: 0,
end: 0,
-2
View File
@@ -84,7 +84,6 @@ type RunFooterOptions = {
theme: RunTheme
keymap: Keymap<Renderable, KeyEvent>
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
diffStyle: RunDiffStyle
onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void>
@@ -326,7 +325,6 @@ export class RunFooter implements FooterApi {
theme: footer.theme,
diffStyle: options.diffStyle,
tuiConfig: options.tuiConfig,
backgroundSubagents: options.backgroundSubagents,
history: footer.history,
agent: options.agentLabel,
onSubmit: footer.handlePrompt,
+1 -4
View File
@@ -89,7 +89,6 @@ type RunFooterViewProps = {
theme: () => RunTheme
diffStyle?: RunDiffStyle
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
history?: () => RunPrompt[]
agent: string
onSubmit: (input: RunPrompt) => boolean
@@ -169,9 +168,7 @@ export function RunFooterView(props: RunFooterViewProps) {
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
})
const foregroundSubagents = createMemo(
() => props.backgroundSubagents && activeTabs().some((item) => !item.background),
)
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
const model = createMemo(() => {
const current = props.currentModel()
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
-4
View File
@@ -1,6 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { truthy } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Effect } from "effect"
import path from "node:path"
@@ -84,9 +83,6 @@ export async function runMini(input: MiniCommandInput) {
files: [],
initialInput,
thinking: true,
backgroundSubagents:
truthy("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") ||
(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === undefined && truthy("OPENCODE_EXPERIMENTAL")),
replay: input.replay ?? true,
replayLimit: input.replayLimit,
demo: input.demo,
-1
View File
@@ -281,7 +281,6 @@ export async function runNonInteractivePrompt(input: Input) {
metadata: {
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
providerCall: current.provider,
providerResult: { executed: event.data.executed, state: event.data.resultState },
+5 -8
View File
@@ -89,12 +89,11 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
!explicitModel && !sessionModel
? await client.model
.default({ location: { directory: cwd, workspace } })
.then((result) =>
result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined,
)
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
: undefined
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (input.variant && !model)
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (model) {
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
const available = await client.model.list({ location: { directory: cwd, workspace } })
@@ -112,9 +111,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
if (!session && input.title !== undefined) {
await client.session.rename({
sessionID: selected.id,
title:
input.title ||
prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
})
}
@@ -200,7 +197,7 @@ async function validateAgent(client: OpenCodeClient, directory: string, name?: s
warning(`failed to list agents${server ? ` from ${server}` : ""}. Falling back to default agent`)
return
}
const agent = agents.find((item) => item.name === name)
const agent = agents.find((item) => item.id === name)
if (!agent) {
warning(`agent "${name}" not found. Falling back to default agent`)
return
@@ -64,7 +64,6 @@ export type LifecycleInput = {
model: RunInput["model"]
variant: string | undefined
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
backgroundSubagents: boolean
onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void>
onQuestionReject: (input: QuestionReject) => void | Promise<void>
@@ -236,7 +235,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
wrote,
keymap,
tuiConfig,
backgroundSubagents: input.backgroundSubagents,
diffStyle: tuiConfig.diff_style ?? "auto",
onPermissionReply: input.onPermissionReply,
onQuestionReply: input.onQuestionReply,
-5
View File
@@ -56,7 +56,6 @@ type RunRuntimeInput = {
files: RunInput["files"]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
@@ -75,7 +74,6 @@ type RunDeferredInput = {
files: RunInput["files"]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
@@ -270,7 +268,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
model: state.model,
variant: state.activeVariant,
tuiConfig: tuiConfigTask,
backgroundSubagents: input.backgroundSubagents,
onPermissionReply: async (next) => {
if (state.demo?.permission(next)) {
return
@@ -876,7 +873,6 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
files: input.files,
initialInput: input.initialInput,
thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
@@ -928,7 +924,6 @@ export async function runInteractiveMode(
files: input.files,
initialInput: input.initialInput,
thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
+6 -8
View File
@@ -16,7 +16,7 @@
// backgrounding is intentionally absent: subagent jobs block the parent
// session, so only whole-session `v2.session.background(parentID)` exists.
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
import type { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2"
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@opencode-ai/tui/util/locale"
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
@@ -54,7 +54,7 @@ export function legacyTool(input: {
callID: tool.id,
tool: tool.name,
}
if (tool.state.status === "pending") {
if (tool.state.status === "streaming") {
return {
...base,
state: { status: "pending", input: {}, raw: tool.state.input },
@@ -83,7 +83,6 @@ export function legacyTool(input: {
metadata: {
structured: tool.state.structured,
content: tool.state.content,
outputPaths: tool.state.outputPaths,
result: tool.state.result,
providerCall,
providerResult,
@@ -179,7 +178,7 @@ export type SubagentTrackerInput = {
export type SubagentTracker = {
main(event: V2Event): void
foreign(sessionID: string, event: V2Event): void
hydrate(next: { messages: SessionMessage[]; active: Record<string, unknown> }): Promise<void>
hydrate(next: { messages: SessionMessageInfo[]; active: Record<string, unknown> }): Promise<void>
select(sessionID: string | undefined): void
snapshot(): FooterSubagentState
}
@@ -316,7 +315,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
messageID,
tool: item,
})
if (item.state.status === "pending") return
if (item.state.status === "streaming") return
child.callIDs.add(item.id)
if (item.state.status === "running") {
setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
@@ -327,7 +326,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
}
const rebuild = (child: ChildState, messages: SessionMessage[]) => {
const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => {
child.frames = []
child.text.clear()
child.projectedText.clear()
@@ -412,7 +411,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
for (const [id, prompt] of pendingPrompts) {
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
}
rebuild(child, structuredClone(response.data).toReversed() as SessionMessage[])
rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[])
for (const [id, tool] of pendingTools) {
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
}
@@ -622,7 +621,6 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: {
+15 -16
View File
@@ -2,7 +2,7 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p
import type {
PermissionRequest,
QuestionRequest,
SessionMessage,
SessionMessageInfo,
SessionMessageAssistantTool,
} from "@opencode-ai/sdk/v2"
import { Event } from "@opencode-ai/schema/event"
@@ -389,7 +389,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
messageID,
tool: item,
})
if (item.state.status === "pending") return
if (item.state.status === "streaming") return
if (item.state.status === "running") {
if (state.tools.get(item.id)?.running) return
state.tools.set(item.id, {
@@ -417,7 +417,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
])
}
const renderMessage = (message: SessionMessage, render: boolean, reuseVisibleWait: boolean) => {
const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => {
if (message.type === "user") {
const waiting = state.wait?.messageID === message.id
if (waiting && state.wait) state.wait.promoted = true
@@ -438,34 +438,34 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
return
}
if (message.type === "shell") {
state.shellCommands.set(message.shell.id, message.shell.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shell.id
state.shellCommands.set(message.shellID, message.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID
const completed = message.time.completed !== undefined
if (!render) {
// Suppressed history: mark settled shells rendered so live redelivery
// stays silent. A still-running shell stays unmarked and renders in
// full when its live shell.ended event arrives.
if (completed) {
state.shellStarted.add(message.shell.id)
state.shellEnded.add(message.shell.id)
state.shellStarted.add(message.shellID)
state.shellEnded.add(message.shellID)
}
return
}
if (!state.shellStarted.has(message.shell.id)) {
state.shellStarted.add(message.shell.id)
if (!state.shellStarted.has(message.shellID)) {
state.shellStarted.add(message.shellID)
write([
shellCommit(message.shell.id, message.shell.command, {
shellCommit(message.shellID, message.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
])
}
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
state.shellEnded.add(message.shell.id)
write(shellTerminal(message.shell.id, message.shell.command, message.shell, message.output))
if (completed && message.output && !state.shellEnded.has(message.shellID)) {
state.shellEnded.add(message.shellID)
write(shellTerminal(message.shellID, message.command, message, message.output))
}
if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
return
}
if (message.type !== "assistant") return
@@ -534,7 +534,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input.sdk.question.list({ sessionID: input.sessionID }),
input.sdk.session.active(),
])
const projected = structuredClone(messages.data).toReversed() as SessionMessage[]
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
state.permissions = permissions.map(permission)
state.questions = questions.map(question)
@@ -762,7 +762,6 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
+2 -2
View File
@@ -119,7 +119,7 @@ type ToolName =
| "bash"
| "write"
| "edit"
| "apply_patch"
| "patch"
| "batch"
| "task"
| "todowrite"
@@ -1094,7 +1094,7 @@ const TOOL_RULES = {
},
permission: permEdit,
},
apply_patch: {
patch: {
view: {
output: false,
final: true,
+1 -1
View File
@@ -113,6 +113,7 @@ export type FooterQueuedPrompt = {
}
export type RunAgent = {
id: string
name: string
description?: string
mode: "subagent" | "primary" | "all"
@@ -135,7 +136,6 @@ export type RunInput = {
files: RunFilePart[]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
demo?: boolean
}
+26 -13
View File
@@ -7,13 +7,12 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Global } from "@opencode-ai/core/global"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AppProcess } from "@opencode-ai/core/process"
import { Flock } from "@opencode-ai/core/util/flock"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { start } from "@opencode-ai/server/process"
import { randomBytes, randomUUID } from "node:crypto"
import path from "node:path"
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
import { Effect, Exit, FileSystem, Logger, Option, Redacted, Schedule, Schema, Scope } from "effect"
import { HttpServer } from "effect/unstable/http"
import { CodeModeHost } from "./code-mode"
import { Env } from "./env"
import { ServiceConfig } from "./services/service-config"
import { Updater } from "./services/updater"
@@ -29,7 +28,7 @@ export type Options = {
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
processEffect(options).pipe(
Effect.provide(Updater.layer),
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, EffectFlock.node]))),
Effect.provide(NodeServices.layer),
),
)
@@ -38,13 +37,15 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
return yield* Effect.scoped(
Effect.gen(function* () {
if (options.mode === "service") {
const service = yield* ServiceConfig.options()
yield* Flock.effect(path.basename(service.file, ".json") + "-process", {
dir: path.dirname(service.file),
staleMs: 3_000,
timeoutMs: 15_000,
})
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
const lockScope = serviceOptions === undefined ? undefined : yield* acquireServiceLock(serviceOptions.file)
if (
serviceOptions !== undefined &&
lockScope !== undefined &&
(yield* Service.discover(serviceOptions)) !== undefined
) {
yield* Scope.close(lockScope, Exit.void)
return
}
const environmentPassword = yield* Env.password
// Keep the lease credential out of the environment inherited by tools.
@@ -64,9 +65,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
port: Option.fromNullishOr(options.port ?? config.port),
password,
replacements: (server) => CodeModeHost.replacements(server, password),
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
if (options.mode === "service") yield* register(address, password)
if (lockScope !== undefined) {
yield* register(address, password)
yield* Scope.close(lockScope, Exit.void)
}
const url = HttpServer.formatAddress(address)
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
@@ -77,6 +80,16 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
)
})
const acquireServiceLock = Effect.fnUntraced(function* (file: string) {
const flock = yield* EffectFlock.Service
const scope = yield* Scope.make()
yield* Effect.addFinalizer((exit) => Scope.close(scope, exit))
yield* flock
.acquire(`service:${file}`, undefined, { staleMs: 3_000, timeoutMs: 3_000 })
.pipe(Effect.provideService(Scope.Scope, scope))
return scope
})
// The latest atomic registration wins. A displaced process notices the new id,
// exits, and cannot remove its successor's registration from its finalizer.
const infoJson = Schema.fromJsonString(Service.Info)
+12
View File
@@ -18,6 +18,7 @@ export function runTui(
const config = TuiConfig.resolve({}, { terminalSuspend: false })
let disposeSlots: (() => void) | undefined
return Effect.gen(function* () {
const runFork = Effect.runForkWith(yield* Effect.context())
const options = { baseUrl: transport.url, headers: transport.headers }
const api = OpenCode.make(options)
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe(
@@ -41,6 +42,17 @@ export function runTui(
reload,
args,
config,
log: (level, message, tags) => {
const effect =
level === "debug"
? Effect.logDebug(message, tags)
: level === "warn"
? Effect.logWarning(message, tags)
: level === "error"
? Effect.logError(message, tags)
: Effect.logInfo(message, tags)
runFork(effect)
},
pluginHost: {
async start(input) {
disposeSlots = await loadBuiltinPlugins(input.api, input.runtime)
+242
View File
@@ -0,0 +1,242 @@
/** @jsxImportSource @opentui/solid */
import { createCliRenderer, RGBA, type CliRenderer, type ColorInput, type ScrollbackWriter } from "@opentui/core"
import { createScrollbackWriter, render, useKeyboard } from "@opentui/solid"
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
import { Show, createSignal } from "solid-js"
registerOpencodeSpinner()
export type TimelineHost = {
readonly signal: AbortSignal
intro(text: string): Promise<void>
item(text: string): Promise<void>
pending(text: string): Promise<void>
success(text: string): Promise<void>
failure(text: string): Promise<void>
outro(text: string): Promise<void>
close(): Promise<void>
}
type RowKind = "intro" | "item" | "success" | "failure" | "outro"
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
const IDLE_TIMEOUT = 1_000
const COLORS = {
accent: RGBA.fromIndex(6),
error: RGBA.fromIndex(1),
foreground: RGBA.defaultForeground(),
muted: RGBA.fromIndex(8),
success: RGBA.fromIndex(2),
}
const ROWS: Record<RowKind, { marker: string; color: ColorInput; connector: boolean }> = {
intro: { marker: "┌", color: COLORS.muted, connector: true },
item: { marker: "●", color: COLORS.accent, connector: true },
success: { marker: "◇", color: COLORS.success, connector: true },
failure: { marker: "■", color: COLORS.error, connector: false },
outro: { marker: "└", color: COLORS.muted, connector: false },
}
function row(kind: RowKind, value: string): ScrollbackWriter {
const style = ROWS[kind]
return createScrollbackWriter(
() => (
<box width="100%" minHeight={1} flexDirection="column">
<box width="100%" minHeight={1} flexDirection="row" gap={1}>
<text fg={style.color} flexShrink={0}>
{style.marker}
</text>
<text fg={COLORS.foreground} wrapMode="word">
{value}
</text>
</box>
<Show when={style.connector}>
<text fg={COLORS.muted}></text>
</Show>
</box>
),
{ startOnNewLine: true, trailingNewline: !style.connector },
)
}
function TimelineFooter(props: { pending: () => string | undefined; cancel: () => void }) {
useKeyboard((event) => {
if (event.name !== "escape" && !(event.ctrl && event.name === "c")) return
event.preventDefault()
props.cancel()
})
return (
<box width="100%" height={1} flexDirection="row" gap={1}>
<Show when={props.pending()}>
{(text) => (
<>
<spinner frames={SPINNER_FRAMES} interval={80} color={COLORS.accent} />
<text fg={COLORS.foreground} wrapMode="none" truncate>
{text()}
</text>
</>
)}
</Show>
</box>
)
}
function bounded(task: Promise<unknown>) {
return new Promise<void>((resolve) => {
const timer = setTimeout(resolve, IDLE_TIMEOUT)
timer.unref()
const finish = () => {
clearTimeout(timer)
resolve()
}
void task.then(finish, finish)
})
}
async function shutdown(renderer: CliRenderer): Promise<void> {
await bounded(renderer.idle())
try {
renderer.externalOutputMode = "passthrough"
} finally {
try {
renderer.screenMode = "main-screen"
} finally {
if (!renderer.isDestroyed) renderer.destroy()
}
}
}
export async function createTimelineHost(): Promise<TimelineHost> {
const stdout = process.stdout
const controller = new AbortController()
const signals: NodeJS.Signals[] = ["SIGINT", "SIGHUP", "SIGQUIT"]
const cancel = () => {
if (!controller.signal.aborted) controller.abort()
}
signals.forEach((signal) => process.on(signal, cancel))
if (!stdout.isTTY || !process.stdin.isTTY) {
let closed = false
let writing = false
let active: Promise<void> | undefined
let closeTask: Promise<void> | undefined
const write = async (kind: RowKind | "pending", text: string) => {
if (closed) throw new Error("timeline closed")
if (writing) throw new Error("timeline write already in progress")
writing = true
try {
const style = kind === "pending" ? undefined : ROWS[kind]
const marker = kind === "pending" ? "." : ROWS[kind].marker
const connector = style?.connector ? "│\n" : ""
active = new Promise<void>((resolve, reject) => {
stdout.write(`${marker} ${text}\n${connector}`, (error) => (error ? reject(error) : resolve()))
})
await active
} finally {
writing = false
active = undefined
}
}
const close = () => {
if (closeTask) return closeTask
closed = true
closeTask = (async () => {
await active?.catch(() => { })
signals.forEach((signal) => process.off(signal, cancel))
})()
return closeTask
}
return {
signal: controller.signal,
intro: (text) => write("intro", text),
item: (text) => write("item", text),
pending: (text) => write("pending", text),
success: (text) => write("success", text),
failure: (text) => write("failure", text),
outro: (text) => write("outro", text),
close,
}
}
let renderer: CliRenderer | undefined
try {
// Start on a fresh row so delayed SSH cursor reports cannot make
// split-footer overwrite the shell command.
process.stdout.write("\n")
renderer = await createCliRenderer({
stdin: process.stdin,
useMouse: false,
autoFocus: false,
openConsoleOnError: false,
exitOnCtrlC: false,
exitSignals: [],
screenMode: "split-footer",
footerHeight: 1,
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
clearOnShutdown: false,
})
const activeRenderer = renderer
const [pending, setPending] = createSignal<string>()
const renderTask = render(() => <TimelineFooter pending={pending} cancel={cancel} />, activeRenderer)
void renderTask.catch(cancel)
await bounded(activeRenderer.idle())
let closed = false
let writing = false
let active: Promise<void> | undefined
let closeTask: Promise<void> | undefined
const write = (kind: RowKind | "pending", text: string) => {
if (closed) return Promise.reject(new Error("timeline closed"))
if (writing) return Promise.reject(new Error("timeline write already in progress"))
writing = true
active = (async () => {
if (kind === "pending") {
setPending(text)
activeRenderer.requestRender()
} else {
if (kind === "success" || kind === "failure" || kind === "outro") setPending(undefined)
activeRenderer.writeToScrollback(row(kind, text))
activeRenderer.requestRender()
}
await bounded(activeRenderer.idle())
})().finally(() => {
writing = false
active = undefined
})
return active
}
const close = () => {
if (closeTask) return closeTask
closed = true
closeTask = (async () => {
await active?.catch(() => { })
try {
await shutdown(activeRenderer)
await bounded(renderTask)
} finally {
signals.forEach((signal) => process.off(signal, cancel))
}
})()
return closeTask
}
return {
signal: controller.signal,
intro: (text) => write("intro", text),
item: (text) => write("item", text),
pending: (text) => write("pending", text),
success: (text) => write("success", text),
failure: (text) => write("failure", text),
outro: (text) => write("outro", text),
close,
}
} catch (error) {
try {
if (renderer) await shutdown(renderer)
} finally {
signals.forEach((signal) => process.off(signal, cancel))
}
throw error
}
}
-39
View File
@@ -1,39 +0,0 @@
import { expect, test } from "bun:test"
import { CodeMode } from "@opencode-ai/codemode"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { CodeModeHost } from "../src/code-mode"
test("exposes the authenticated OpenCode API through CodeMode", async () => {
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
const client = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push({ url: request.url, authorization: request.headers.authorization })
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json(
{ healthy: true, version: "test", pid: 1 },
{ headers: { "content-type": "application/json" } },
),
),
)
}),
)
const result = await CodeMode.make({ tools: CodeModeHost.makeTools(client, "secret") })
.execute("return await tools.opencode.v2.health.get({})")
.pipe(Effect.runPromise)
expect(result).toEqual({
ok: true,
value: { healthy: true, version: "test", pid: 1 },
toolCalls: [{ name: "opencode.v2.health.get" }],
})
expect(requests).toEqual([
{
url: "http://opencode.local/api/health",
authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
},
])
})
+46 -1
View File
@@ -1,7 +1,8 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { Global } from "@opencode-ai/core/global"
import { expect, test } from "bun:test"
import { Effect } from "effect"
import { Effect, Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
@@ -24,3 +25,47 @@ test("local channel stores service config with the local service filename", asyn
await fs.rm(root, { recursive: true, force: true })
}
})
test("concurrent service processes elect one server", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
const env = {
...process.env,
HOME: root,
OPENCODE_DB: path.join(root, "opencode.db"),
OPENCODE_TEST_HOME: root,
XDG_CACHE_HOME: path.join(root, "cache"),
XDG_CONFIG_HOME: path.join(root, "config"),
XDG_DATA_HOME: path.join(root, "data"),
XDG_STATE_HOME: path.join(root, "state"),
}
const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"]
const first = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
const second = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
try {
const registration = path.join(root, "state", "opencode", "service-local.json")
const info = await waitForInfo(registration)
const winner = info.pid === first.pid ? first : second
const loser = info.pid === first.pid ? second : first
const exited = await Promise.race([loser.exited.then(() => true), Bun.sleep(10_000).then(() => false)])
expect(exited).toBe(true)
expect(winner.exitCode).toBe(null)
} finally {
first.kill("SIGTERM")
second.kill("SIGTERM")
await Promise.all([first.exited, second.exited])
await fs.rm(root, { recursive: true, force: true })
}
})
async function waitForInfo(file: string) {
for (let attempt = 0; attempt < 200; attempt++) {
const value = await Bun.file(file)
.json()
.catch(() => undefined)
if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value)
await Bun.sleep(50)
}
throw new Error("Timed out waiting for service registration")
}
+2 -3
View File
@@ -3,15 +3,14 @@ import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from
import {
ClientApi,
effectOmitEndpoints,
endpointNames,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Effect } from "effect"
import { fileURLToPath } from "url"
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
await Effect.runPromise(
Effect.all(
-1
View File
@@ -1,7 +1,6 @@
export {
ClientApi,
effectOmitEndpoints,
endpointNames,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
+142 -113
View File
@@ -111,157 +111,167 @@ export type Endpoint4_8Input = {
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
export type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
}
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
export type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly command: Endpoint4_10Request["payload"]["command"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"]
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
export type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly skill: Endpoint4_11Request["payload"]["skill"]
readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
}
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
export type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly text: Endpoint4_12Request["payload"]["text"]
readonly description?: Endpoint4_12Request["payload"]["description"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
readonly id?: Endpoint4_12Request["payload"]["id"]
readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly resume?: Endpoint4_12Request["payload"]["resume"]
}
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
export type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly id?: Endpoint4_13Request["payload"]["id"]
readonly command: Endpoint4_13Request["payload"]["command"]
readonly text: Endpoint4_13Request["payload"]["text"]
readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
}
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
export type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
}
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_16Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"]
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint4_15Input = {
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly id?: Endpoint4_15Request["payload"]["id"]
}
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_17Request["payload"]["files"]
}
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
export type Endpoint4_20Output = EffectValue<
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
export type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
export type Endpoint4_21Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
>["data"]
export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint4_20Input,
) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"]
}
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint4_21Input,
) => Effect.Effect<Endpoint4_21Output, E>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_22Request["payload"]["value"]
}
export type Endpoint4_22Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint4_22Input,
) => Effect.Effect<Endpoint4_22Output, E>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
export type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"]
readonly follow?: Endpoint4_23Request["query"]["follow"]
readonly key: Endpoint4_23Request["params"]["key"]
}
export type Endpoint4_23Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint4_23Input) => Stream.Stream<Endpoint4_23Output, E>
export type Endpoint4_23Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint4_23Input,
) => Effect.Effect<Endpoint4_23Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
export type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_24Request["query"]["follow"]
}
export type Endpoint4_24Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint4_24Input) => Stream.Stream<Endpoint4_24Output, E>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_26Input = {
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
readonly messageID: Endpoint4_26Request["params"]["messageID"]
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_27Input = {
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_27Request["params"]["messageID"]
}
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
export interface SessionApi<E = never> {
readonly list: SessionListOperation<E>
@@ -273,6 +283,7 @@ export interface SessionApi<E = never> {
readonly switchAgent: SessionSwitchAgentOperation<E>
readonly switchModel: SessionSwitchModelOperation<E>
readonly rename: SessionRenameOperation<E>
readonly move: SessionMoveOperation<E>
readonly prompt: SessionPromptOperation<E>
readonly command: SessionCommandOperation<E>
readonly skill: SessionSkillOperation<E>
@@ -280,9 +291,11 @@ export interface SessionApi<E = never> {
readonly shell: SessionShellOperation<E>
readonly compact: SessionCompactOperation<E>
readonly wait: SessionWaitOperation<E>
readonly revertStage: SessionRevertStageOperation<E>
readonly revertClear: SessionRevertClearOperation<E>
readonly revertCommit: SessionRevertCommitOperation<E>
readonly revert: {
readonly stage: SessionRevertStageOperation<E>
readonly clear: SessionRevertClearOperation<E>
readonly commit: SessionRevertCommitOperation<E>
}
readonly context: SessionContextOperation<E>
readonly instructions: {
readonly entry: {
@@ -427,11 +440,15 @@ export type IntegrationAttemptCancelOperation<E = never> = (
export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E>
readonly connectKey: IntegrationConnectKeyOperation<E>
readonly connectOauth: IntegrationConnectOauthOperation<E>
readonly attemptStatus: IntegrationAttemptStatusOperation<E>
readonly attemptComplete: IntegrationAttemptCompleteOperation<E>
readonly attemptCancel: IntegrationAttemptCancelOperation<E>
readonly connect: {
readonly key: IntegrationConnectKeyOperation<E>
readonly oauth: IntegrationConnectOauthOperation<E>
}
readonly attempt: {
readonly status: IntegrationAttemptStatusOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
}
}
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -439,8 +456,16 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
export type ServerMcpResourceCatalogOperation<E = never> = (
input?: Endpoint10_1Input,
) => Effect.Effect<Endpoint10_1Output, E>
export interface ServerMcpApi<E = never> {
readonly list: ServerMcpListOperation<E>
readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
}
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
@@ -490,7 +515,7 @@ export interface ProjectApi<E = never> {
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
export type FormRequestListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
@@ -501,7 +526,7 @@ type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.cre
export type Endpoint13_2Input = {
readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
readonly id?: Endpoint13_2Request["payload"]["id"]
readonly title?: Endpoint13_2Request["payload"]["title"]
readonly title: Endpoint13_2Request["payload"]["title"]
readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
readonly mode: Endpoint13_2Request["payload"]["mode"]
readonly fields?: Endpoint13_2Request["payload"]["fields"]
@@ -544,7 +569,7 @@ export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
export interface FormApi<E = never> {
readonly listRequests: FormListRequestsOperation<E>
readonly request: { readonly list: FormRequestListOperation<E> }
readonly list: FormListOperation<E>
readonly create: FormCreateOperation<E>
readonly get: FormGetOperation<E>
@@ -556,7 +581,7 @@ export interface FormApi<E = never> {
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
export type PermissionListRequestsOperation<E = never> = (
export type PermissionRequestListOperation<E = never> = (
input?: Endpoint14_0Input,
) => Effect.Effect<Endpoint14_0Output, E>
@@ -565,14 +590,14 @@ export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["quer
export type Endpoint14_1Output = EffectValue<
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
>["data"]
export type PermissionListSavedOperation<E = never> = (
export type PermissionSavedListOperation<E = never> = (
input?: Endpoint14_1Input,
) => Effect.Effect<Endpoint14_1Output, E>
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
export type PermissionRemoveSavedOperation<E = never> = (
export type PermissionSavedRemoveOperation<E = never> = (
input: Endpoint14_2Input,
) => Effect.Effect<Endpoint14_2Output, E>
@@ -620,9 +645,8 @@ export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permis
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
export interface PermissionApi<E = never> {
readonly listRequests: PermissionListRequestsOperation<E>
readonly listSaved: PermissionListSavedOperation<E>
readonly removeSaved: PermissionRemoveSavedOperation<E>
readonly request: { readonly list: PermissionRequestListOperation<E> }
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
readonly create: PermissionCreateOperation<E>
readonly list: PermissionListOperation<E>
readonly get: PermissionGetOperation<E>
@@ -791,7 +815,7 @@ export interface ShellApi<E = never> {
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
export type QuestionListRequestsOperation<E = never> = (
export type QuestionRequestListOperation<E = never> = (
input?: Endpoint21_0Input,
) => Effect.Effect<Endpoint21_0Output, E>
@@ -818,7 +842,7 @@ export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.questi
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
export interface QuestionApi<E = never> {
readonly listRequests: QuestionListRequestsOperation<E>
readonly request: { readonly list: QuestionRequestListOperation<E> }
readonly list: QuestionListOperation<E>
readonly reply: QuestionReplyOperation<E>
readonly reject: QuestionRejectOperation<E>
@@ -888,10 +912,15 @@ export interface VcsApi<E = never> {
}
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export interface DebugApi<E = never> {
readonly location: DebugLocationOperation<E>
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
}
export interface AppApi<E = never> {
+151 -123
View File
@@ -141,15 +141,27 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
Effect.mapError(mapClientError),
)
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly id?: Endpoint4_9Request["payload"]["id"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
}
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
raw["session.move"]({
params: { sessionID: input["sessionID"] },
payload: { destination: input["destination"], moveChanges: input["moveChanges"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
raw["session.prompt"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
@@ -158,20 +170,20 @@ const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Inp
Effect.map((value) => value.data),
)
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly command: Endpoint4_10Request["payload"]["command"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
}
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
raw["session.command"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -190,67 +202,73 @@ const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10I
Effect.map((value) => value.data),
)
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly skill: Endpoint4_11Request["payload"]["skill"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly id?: Endpoint4_12Request["payload"]["id"]
readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly resume?: Endpoint4_12Request["payload"]["resume"]
}
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.skill"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly text: Endpoint4_12Request["payload"]["text"]
readonly description?: Endpoint4_12Request["payload"]["description"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
}
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly id?: Endpoint4_13Request["payload"]["id"]
readonly command: Endpoint4_13Request["payload"]["command"]
readonly text: Endpoint4_13Request["payload"]["text"]
readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
}
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
}
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.shell"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], command: input["command"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_15Input = {
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly id?: Endpoint4_15Request["payload"]["id"]
}
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_16Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"]
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_17Request["payload"]["files"]
}
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] },
@@ -259,61 +277,61 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I
Effect.map((value) => value.data),
)
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"]
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_22Request["payload"]["value"]
}
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly key: Endpoint4_23Request["params"]["key"]
}
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"]
readonly follow?: Endpoint4_23Request["query"]["follow"]
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_24Request["query"]["follow"]
}
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
Stream.unwrap(
raw["session.log"]({
params: { sessionID: input["sessionID"] },
@@ -324,22 +342,22 @@ const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23I
),
)
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_26Input = {
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
readonly messageID: Endpoint4_26Request["params"]["messageID"]
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_27Input = {
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_27Request["params"]["messageID"]
}
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -355,22 +373,21 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
switchAgent: Endpoint4_6(raw),
switchModel: Endpoint4_7(raw),
rename: Endpoint4_8(raw),
prompt: Endpoint4_9(raw),
command: Endpoint4_10(raw),
skill: Endpoint4_11(raw),
synthetic: Endpoint4_12(raw),
shell: Endpoint4_13(raw),
compact: Endpoint4_14(raw),
wait: Endpoint4_15(raw),
revertStage: Endpoint4_16(raw),
revertClear: Endpoint4_17(raw),
revertCommit: Endpoint4_18(raw),
context: Endpoint4_19(raw),
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } },
log: Endpoint4_23(raw),
interrupt: Endpoint4_24(raw),
background: Endpoint4_25(raw),
message: Endpoint4_26(raw),
move: Endpoint4_9(raw),
prompt: Endpoint4_10(raw),
command: Endpoint4_11(raw),
skill: Endpoint4_12(raw),
synthetic: Endpoint4_13(raw),
shell: Endpoint4_14(raw),
compact: Endpoint4_15(raw),
wait: Endpoint4_16(raw),
revert: { stage: Endpoint4_17(raw), clear: Endpoint4_18(raw), commit: Endpoint4_19(raw) },
context: Endpoint4_20(raw),
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
log: Endpoint4_24(raw),
interrupt: Endpoint4_25(raw),
background: Endpoint4_26(raw),
message: Endpoint4_27(raw),
})
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
@@ -517,11 +534,8 @@ const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
list: Endpoint9_0(raw),
get: Endpoint9_1(raw),
connectKey: Endpoint9_2(raw),
connectOauth: Endpoint9_3(raw),
attemptStatus: Endpoint9_4(raw),
attemptComplete: Endpoint9_5(raw),
attemptCancel: Endpoint9_6(raw),
connect: { key: Endpoint9_2(raw), oauth: Endpoint9_3(raw) },
attempt: { status: Endpoint9_4(raw), complete: Endpoint9_5(raw), cancel: Endpoint9_6(raw) },
})
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -529,7 +543,15 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({
list: Endpoint10_0(raw),
resource: { catalog: Endpoint10_1(raw) },
})
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
type Endpoint11_0Input = {
@@ -599,7 +621,7 @@ type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.cre
type Endpoint13_2Input = {
readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
readonly id?: Endpoint13_2Request["payload"]["id"]
readonly title?: Endpoint13_2Request["payload"]["title"]
readonly title: Endpoint13_2Request["payload"]["title"]
readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
readonly mode: Endpoint13_2Request["payload"]["mode"]
readonly fields?: Endpoint13_2Request["payload"]["fields"]
@@ -666,7 +688,7 @@ const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Inpu
)
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
listRequests: Endpoint13_0(raw),
request: { list: Endpoint13_0(raw) },
list: Endpoint13_1(raw),
create: Endpoint13_2(raw),
get: Endpoint13_3(raw),
@@ -754,9 +776,8 @@ const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14
}).pipe(Effect.mapError(mapClientError))
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
listRequests: Endpoint14_0(raw),
listSaved: Endpoint14_1(raw),
removeSaved: Endpoint14_2(raw),
request: { list: Endpoint14_0(raw) },
saved: { list: Endpoint14_1(raw), remove: Endpoint14_2(raw) },
create: Endpoint14_3(raw),
list: Endpoint14_4(raw),
get: Endpoint14_5(raw),
@@ -989,7 +1010,7 @@ const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3
)
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
listRequests: Endpoint21_0(raw),
request: { list: Endpoint21_0(raw) },
list: Endpoint21_1(raw),
reply: Endpoint21_2(raw),
reject: Endpoint21_3(raw),
@@ -1069,7 +1090,14 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
})
const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]),
+244 -179
View File
@@ -23,6 +23,8 @@ import type {
SessionSwitchModelOutput,
SessionRenameInput,
SessionRenameOutput,
SessionMoveInput,
SessionMoveOutput,
SessionPromptInput,
SessionPromptOutput,
SessionCommandInput,
@@ -87,6 +89,8 @@ import type {
IntegrationAttemptCancelOutput,
ServerMcpListInput,
ServerMcpListOutput,
ServerMcpResourceCatalogInput,
ServerMcpResourceCatalogOutput,
CredentialUpdateInput,
CredentialUpdateOutput,
CredentialRemoveInput,
@@ -96,8 +100,8 @@ import type {
ProjectCurrentOutput,
ProjectDirectoriesInput,
ProjectDirectoriesOutput,
FormListRequestsInput,
FormListRequestsOutput,
FormRequestListInput,
FormRequestListOutput,
FormListInput,
FormListOutput,
FormCreateInput,
@@ -110,12 +114,12 @@ import type {
FormReplyOutput,
FormCancelInput,
FormCancelOutput,
PermissionListRequestsInput,
PermissionListRequestsOutput,
PermissionListSavedInput,
PermissionListSavedOutput,
PermissionRemoveSavedInput,
PermissionRemoveSavedOutput,
PermissionRequestListInput,
PermissionRequestListOutput,
PermissionSavedListInput,
PermissionSavedListOutput,
PermissionSavedRemoveInput,
PermissionSavedRemoveOutput,
PermissionCreateInput,
PermissionCreateOutput,
PermissionListInput,
@@ -157,8 +161,8 @@ import type {
ShellOutputOutput,
ShellRemoveInput,
ShellRemoveOutput,
QuestionListRequestsInput,
QuestionListRequestsOutput,
QuestionRequestListInput,
QuestionRequestListOutput,
QuestionListInput,
QuestionListOutput,
QuestionReplyInput,
@@ -177,7 +181,9 @@ import type {
VcsStatusOutput,
VcsDiffInput,
VcsDiffOutput,
DebugLocationOutput,
DebugLocationListOutput,
DebugLocationEvictInput,
DebugLocationEvictOutput,
} from "./types"
import { ClientError } from "./client-error"
@@ -485,6 +491,18 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
request<SessionMoveOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
body: { destination: input["destination"], moveChanges: input["moveChanges"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionPromptOutput }>(
{
@@ -536,7 +554,12 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
body: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
@@ -578,40 +601,42 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionRevertStageOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
successStatus: 200,
declaredStatuses: [404, 409, 500, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
successStatus: 204,
declaredStatuses: [404, 409, 500, 400, 401],
empty: true,
},
requestOptions,
),
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
request<SessionRevertCommitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204,
declaredStatuses: [404, 409, 400, 401],
empty: true,
},
requestOptions,
),
revert: {
stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionRevertStageOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
successStatus: 200,
declaredStatuses: [404, 409, 500, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
successStatus: 204,
declaredStatuses: [404, 409, 500, 400, 401],
empty: true,
},
requestOptions,
),
commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
request<SessionRevertCommitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204,
declaredStatuses: [404, 409, 400, 401],
empty: true,
},
requestOptions,
),
},
context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionContextOutput }>(
{
@@ -813,69 +838,73 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
query: { location: input["location"] },
body: { key: input["key"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
connectOauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
query: { location: input["location"] },
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
attemptStatus: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{
method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
attemptComplete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
{
method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
attemptCancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
{
method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
connect: {
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
query: { location: input["location"] },
body: { key: input["key"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
query: { location: input["location"] },
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
},
attempt: {
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{
method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
{
method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
{
method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
},
"server.mcp": {
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
@@ -890,6 +919,20 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
resource: {
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
request<ServerMcpResourceCatalogOutput>(
{
method: "GET",
path: `/api/mcp/resource`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
},
credential: {
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
@@ -950,18 +993,20 @@ export function make(options: ClientOptions) {
),
},
form: {
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
request<FormListRequestsOutput>(
{
method: "GET",
path: `/api/form/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
request: {
list: (input?: FormRequestListInput, requestOptions?: RequestOptions) =>
request<FormRequestListOutput>(
{
method: "GET",
path: `/api/form/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
list: (input: FormListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: FormListOutput }>(
{
@@ -1039,41 +1084,45 @@ export function make(options: ClientOptions) {
),
},
permission: {
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
request<PermissionListRequestsOutput>(
{
method: "GET",
path: `/api/permission/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
listSaved: (input?: PermissionListSavedInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionListSavedOutput }>(
{
method: "GET",
path: `/api/permission/saved`,
query: { projectID: input?.["projectID"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
removeSaved: (input: PermissionRemoveSavedInput, requestOptions?: RequestOptions) =>
request<PermissionRemoveSavedOutput>(
{
method: "DELETE",
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
request: {
list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) =>
request<PermissionRequestListOutput>(
{
method: "GET",
path: `/api/permission/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
saved: {
list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionSavedListOutput }>(
{
method: "GET",
path: `/api/permission/saved`,
query: { projectID: input?.["projectID"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) =>
request<PermissionSavedRemoveOutput>(
{
method: "DELETE",
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionCreateOutput }>(
{
@@ -1355,18 +1404,20 @@ export function make(options: ClientOptions) {
),
},
question: {
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) =>
request<QuestionListRequestsOutput>(
{
method: "GET",
path: `/api/question/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
request: {
list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
request<QuestionRequestListOutput>(
{
method: "GET",
path: `/api/question/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: QuestionListOutput }>(
{
@@ -1483,17 +1534,31 @@ export function make(options: ClientOptions) {
),
},
debug: {
location: (requestOptions?: RequestOptions) =>
request<DebugLocationOutput>(
{
method: "GET",
path: `/api/debug/location`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
location: {
list: (requestOptions?: RequestOptions) =>
request<DebugLocationListOutput>(
{
method: "GET",
path: `/api/debug/location`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) =>
request<DebugLocationEvictOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
},
}
}
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Api } from "@opencode-ai/server/api"
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
import { ClientApi, groupNames, promiseOmitEndpoints } from "../src/contract"
const Client = await import("../src/effect")
@@ -38,7 +38,7 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
expect(ProjectV2.Directory).toBe(Project.Directory)
expect(ProjectV2.Directories).toBe(Project.Directories)
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
expect(Api.groups["server.session"].identifier).toBe("server.session")
expect(Api.groups["server.project"].identifier).toBe("server.project")
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
@@ -49,8 +49,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
})
test("client and Server contracts generate identically", () => {
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints })
const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
expect(emitPromise(client)).toEqual(emitPromise(server))
})
+27 -9
View File
@@ -33,16 +33,11 @@ test("exposes every standard HTTP API group", () => {
"debug",
])
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual([
"list",
"get",
"connectKey",
"connectOauth",
"attemptStatus",
"attemptComplete",
"attemptCancel",
])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
@@ -50,6 +45,29 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
})
test("MCP resource catalog uses the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input) => {
request = input instanceof Request ? input : new Request(input)
return Response.json({
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
data: {
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
},
})
},
})
const result = await client["server.mcp"].resource.catalog({ location: { directory: "/tmp/project" } })
expect(result.data.resources[0]?.uri).toBe("docs://readme")
expect(request?.method).toBe("GET")
expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
})
test("file.read returns binary content from the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
+4 -4
View File
@@ -62,7 +62,7 @@ const result =
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
Successful result values are JSON-safe data. An explicit `return` produces the program result; when it is omitted, the final executable top-level expression is returned as a model-friendly REPL convenience. Otherwise reaching the end produces `null`. Returned `undefined` and nested `undefined` values are normalized to `null` as well.
## API
@@ -241,17 +241,17 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports:
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
- Common array, string, number, `Object`, `Math`, and `JSON` operations, including primitive-number `valueOf`, the standard non-finite `Number` constants, and host-backed `Math.random`. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
- URL helpers - `URL` resolution and mutation, linked `URLSearchParams`, `URL.canParse`/`URL.parse`, URI and URI-component encoding/decoding, and query parameter construction, lookup, mutation, sorting, callbacks, and materialization. URLSearchParams iteration methods return arrays, matching the Map/Set convention.
- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve`/`Promise.reject` construct settled promises. `Promise.allSettled` rejection reasons are the same plain `{ name?, message }` data a `catch` binding sees, and `Promise.race` interrupts its losing in-flight calls. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
- First-class promises - an un-awaited `tools.ns.tool(...)` is a promise value whose call starts immediately on a supervised fiber; `await` resolves it (awaiting a non-promise value is a no-op, and `return tools.ns.tool(...)` resolves like an async-function return). Promises support `then`/`catch`/`finally`, including returned-promise flattening and rejection recovery. `Promise.all`, `Promise.allSettled`, and `Promise.race` accept any array mixing promises and plain values (built inline, beforehand, or via spread); `Promise.resolve` preserves an existing promise or wraps a plain value, and `Promise.reject` creates a rejected promise. `Promise.allSettled` rejection reasons use the same values a `catch` binding sees, and `Promise.race` lets losing promises continue. At most 8 tool calls run concurrently. When a program completes, still-running un-awaited calls are awaited before the execution ends; a failure from a call that was never awaited surfaces as an unhandled-rejection diagnostic.
- `throw value` and `throw new Error(message)` for explicit program failure. `Error` (and `TypeError`/`RangeError`/`SyntaxError`/`ReferenceError`/`EvalError`/`URIError`) are real constructors, callable with or without `new`; error values are plain `{ name, message }` data that additionally satisfy `instanceof Error` (a specific type matches itself and `Error`, as in JS). Every caught failure - thrown errors, interpreter runtime errors, and tool failures - is `instanceof Error` in a `catch` block; a thrown non-error value (`throw "text"`) is not, matching JS. Caught failures carry the `name` the equivalent real-JS failure would have - `JSON.parse` and invalid regex patterns produce a `SyntaxError` (satisfying `instanceof SyntaxError`), an unknown identifier a `ReferenceError`, assigning to a constant a `TypeError`, a bad `normalize` form a `RangeError`; failures with no specific analogue (including tool failures) are named `"Error"`. `instanceof` also recognizes `Date`, `RegExp`, `Map`, `Set`, `URL`, `URLSearchParams`, `Array`, `Object`, and `Promise`; any other right-hand side is a catchable error.
Inside a program, standard-library values stay live everywhere: the internal data checkpoints (`Object.*` helpers, spread, coercion inputs) preserve the instances, so `Object.values({ d: date })[0].getTime()` and a spread copy of an object holding a Map keep working. Only at the host boundary (final result, tool arguments, `JSON.stringify`) do they serialize exactly as `JSON.stringify` would: Date and URL become strings (an invalid Date becomes `null`), while RegExp, Map, Set, and URLSearchParams become `{}`. Promise values never cross a data boundary: an un-awaited promise in a result or tool argument produces a diagnostic that says to await it, instead of serializing to `{}`.
It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), promise chaining (`.then`/`.catch`/`.finally` - `await` with `try`/`catch` is the supported style), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available.
It does not expose `eval`, dynamic imports, modules, classes, generators, timers, host globals, prototype mutation, custom promise constructors (`new Promise`), or arbitrary method calls. Unsupported syntax returns an `UnsupportedSyntax` diagnostic with a source location when available.
CodeMode is an orchestration language, not a general JavaScript runtime.
+62 -7
View File
@@ -63,8 +63,10 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
### Tool execution
Calling a tool starts its Effect eagerly on a supervised fiber. The returned sandbox promise is run-once and can be
awaited directly or through the supported `Promise` combinators. At most eight tool calls execute concurrently.
Unfinished calls are drained before successful program completion, and an unhandled call failure becomes a diagnostic.
awaited directly, chained with `then`/`catch`/`finally`, or passed through the supported `Promise` combinators. At most
eight tool calls execute concurrently.
Unfinished tracked promises are drained before successful program completion, and an unhandled rejection becomes a
diagnostic.
The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no
defaults because budgets are host policy. The interpreter also enforces fixed internal boundaries for tool-call
@@ -109,6 +111,62 @@ MCP tools use this canonical path: they register as grouped tools and are deferr
output schemas are preserved in generated signatures. Direct Core tools remain direct and are not ambient globals
inside CodeMode.
## Promise Status
The runtime currently provides eager, run-once promises for tool calls and async functions; `await`;
`then`/`catch`/`finally`; and chainable `all`/`allSettled`/`race`/`resolve`/`reject`. `Promise.all` rejects promptly while
siblings continue, and `Promise.race` leaves losers running as JavaScript does. Tracked work remains supervised in one
execution scope, at most eight tool calls run concurrently, and ordinary success or failure drains unfinished work before
closing. Timeout and external interruption cancel immediately instead.
### Confirmed defects
- [ ] Align handler callability with the values CodeMode reports as functions, or document the narrower callback
allowlist. For example, unsupported constructor-like callables are currently treated as absent handlers.
- [ ] Run synchronous Promise reactions to completion. Reactions currently start in FIFO order but can interleave when
Effect auto-yields a long synchronous handler.
- [ ] Give `Promise.race` settlement the same reaction turn as native promises. Its result currently settles before an
independently queued reaction in several Test262 ordering cases.
- [ ] Drain queued reactions before classifying rejected sources as unhandled. A handler attached by an already-queued
reaction can currently lose to execution-boundary rejection reporting.
### Deliberate deviations and open decisions
- CodeMode drains unfinished work before ordinary success or failure closes. This keeps tool effects supervised, but a
race loser or fail-fast `Promise.all` sibling that never settles can hold execution open indefinitely when the host
supplies no timeout.
- Promise resolution unwraps only `SandboxPromise`; arbitrary `{ then(resolve, reject) }` values remain data. Full
thenable assimilation requires internal callable resolver values, first-settlement arbitration, recursive adoption,
and cycle detection. Decide whether that machinery belongs in the bounded runtime.
- `new Promise`, `Promise.any`, resolver APIs, subclasses/species, and the broader prototype surface are unavailable.
Consider `Promise.any` independently; custom constructors and subclassing are not current goals.
- Combinators currently accept arrays plus CodeMode's spreadable strings, Maps, and Sets, while documentation and
diagnostics describe array inputs. Choose and document one contract.
- `Promise.race([])` raises a clear error instead of creating a permanently pending promise.
- Rejection tracking is execution-scoped and checked at drain time, not an ECMAScript microtask-level unhandled
rejection model.
### Covered regressions
- Nested unreturned tool calls remain alive after an async function or `then` handler settles.
- Abandoned failing tool calls, async functions, and immediate `Promise.reject` values report unhandled rejections.
- Ordinary program failure drains pending work and preserves the original error; a rejecting race winner drains its slow
loser.
- Timeouts interrupt all in-flight promise fibers with parallel teardown, while host interruption propagates instead of
becoming a diagnostic.
- Promise reactions and plain, pending, or settled `await` continuations start in deterministic FIFO order. Nested
reactions preserve enqueue order, while an async reaction can suspend without blocking the next queued reaction.
- Awaiting the same promise twice settles it once.
- Sparse combinator inputs consume holes as explicit `undefined` values.
### Missing coverage
- Nested unreturned work from `catch` and `finally` handlers.
- Abandoned chained and combinator rejections.
- External interruption while handled pending work remains.
- Never-settling race losers and fail-fast `Promise.all` siblings under an explicit timeout.
- Shared or duplicate promises across combinators, discarded inner chains, and detailed reaction ordering.
## Intentionally Unsupported
These are product boundaries rather than DSL backlog:
@@ -130,7 +188,7 @@ represent accurately rather than guessing semantics.
| Keep an owned tree-walking interpreter. | The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit. |
| Treat schemas as the model-facing interface. | Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability. |
| Keep authority host-owned. | CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy. |
| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid. |
| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls must remain valid. |
| Start tool promises eagerly and supervise them. | This preserves normal call-time parallelism while giving each call run-once settlement and interruption safety. |
| Keep files outside the sandbox value space. | Models should compose structured data without routing binary payloads through generated code or context. |
| Treat `execute` as the model-facing invocation boundary. | Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities. |
@@ -148,9 +206,6 @@ the adapter TODO. Delete entries when completed.
The supported JavaScript subset should grow when common model-generated code improves tool orchestration. These are
current omissions to implement, not intentional product boundaries.
- [ ] Design proper multi-stage promise pipelines. Supporting `.then`, `.catch`, and `.finally` should preserve promise
assimilation, cancellation, failure handling, and concurrent per-item pipelines rather than adding syntax-only
shims. Consider `Promise.any` in the same pass.
- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and
collection values, then extend it to bounded host streams when a stream boundary exists.
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
@@ -158,7 +213,7 @@ current omissions to implement, not intentional product boundaries.
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
composition methods, and `Array.prototype.toSpliced`.
- [ ] Decide whether nondeterministic `Math.random` and iterable `Math.sumPrecise` belong in the runtime.
- [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
defects are distinguishable without leaking private causes.
+1 -2
View File
@@ -30,7 +30,6 @@ export type Binding = {
export type StatementResult =
| { kind: "none" }
| { kind: "value"; value: unknown }
| { kind: "return"; value: unknown }
| { kind: "break" }
| { kind: "continue" }
@@ -123,7 +122,7 @@ export type DiagnosticKind =
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
export const supportedSyntaxMessage =
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported - use await with try/catch)."
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await or .then/.catch/.finally), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls."
export class InterpreterRuntimeError extends Error {
readonly node?: AstNode
+289 -167
View File
@@ -1,5 +1,5 @@
import { parse } from "acorn"
import { Cause, Effect, Exit, Fiber, Semaphore } from "effect"
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Scope, Semaphore } from "effect"
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
import {
copyIn,
@@ -148,6 +148,11 @@ const parseProgram = (code: string): ProgramNode => {
return parsed as ProgramNode
}
type PromiseReaction<R> = {
readonly effect: Effect.Effect<unknown, unknown, R>
readonly settlement: Deferred.Deferred<unknown, unknown>
}
const publicErrorMessage = (message: string): string =>
message.replace(/\/(?:Users|home|private|tmp|var\/folders)\/[^\s"'`]+/g, "<redacted-path>")
@@ -219,7 +224,7 @@ const normalizeError = (error: unknown): Diagnostic => {
}
}
// Shared by catch bindings, Promise.allSettled rejection reasons, and Promise.race losers.
// Shared by catch bindings and Promise.allSettled rejection reasons.
const caughtErrorValue = (thrown: unknown): unknown => {
if (thrown instanceof ProgramThrow) return thrown.value
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
@@ -530,17 +535,29 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
if (args[0] instanceof SandboxURLSearchParams) {
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
}
const source = boundedData(args[0], "Array.from input")
const source = args[0]
if (source instanceof SandboxPromise) {
throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.",
node,
"InvalidDataValue",
)
}
if (typeof source === "string") return Array.from(source)
if (Array.isArray(source)) return [...source]
if (
source !== null &&
typeof source === "object" &&
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
typeof (source as { length?: unknown }).length === "number"
) {
return Array.from(source as ArrayLike<unknown>)
}
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
throw new InterpreterRuntimeError(
"Array.from expects an array, string, Map, Set, or array-like value.",
node,
"InvalidDataValue",
)
}
default:
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
@@ -607,9 +624,13 @@ class Interpreter<R> {
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
private readonly logs: Array<string>
private lastValue: unknown
// Every promise fiber belongs to the execution rather than the async function or handler
// that happened to create it. The execution scope still interrupts all work on teardown.
private readonly promiseScope: Scope.Scope
private readonly reactionQueue: Queue.Queue<PromiseReaction<R>>
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
private readonly callPermits: Semaphore.Semaphore
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
// Fiber-backed promises whose settlement no program construct has observed yet. Ordinary
// program completion drains these (like a runtime waiting on in-flight work at exit) and
// surfaces a never-awaited failure as an unhandled-rejection diagnostic.
private readonly pendingSettlements: Set<SandboxPromise>
@@ -617,6 +638,8 @@ class Interpreter<R> {
constructor(
invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
promiseScope: Scope.Scope,
reactionQueue: Queue.Queue<PromiseReaction<R>>,
logs: Array<string> = [],
shared?: { callPermits: Semaphore.Semaphore; pendingSettlements: Set<SandboxPromise> },
) {
@@ -626,6 +649,8 @@ class Interpreter<R> {
this.toolKeys = toolKeys
this.logs = logs
this.lastValue = undefined
this.promiseScope = promiseScope
this.reactionQueue = reactionQueue
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
@@ -668,16 +693,18 @@ class Interpreter<R> {
// top-level declarations (`let undefined = 5`, `const Object = ...`) shadow builtins like
// JS module scope, instead of colliding with the seeded globals.
this.pushScope()
return Effect.gen(function* () {
const evaluate = Effect.gen(function* () {
self.hoistFunctions(program.body)
let value: unknown = undefined
let returned = false
for (const statement of program.body) {
for (const [index, statement] of program.body.entries()) {
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
value = yield* self.evaluateExpression(getNode(statement, "expression"))
break
}
const result = yield* self.evaluateStatement(statement)
if (result.kind === "return") {
value = result.value
returned = true
break
}
@@ -685,41 +712,50 @@ class Interpreter<R> {
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
}
if (result.kind === "value") {
self.lastValue = result.value
}
}
if (!returned) value = self.lastValue
// The program body runs inside an implicit async function, so a returned promise
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
// without an explicit await, exactly as in JS.
if (value instanceof SandboxPromise) value = yield* self.settlePromise(value)
yield* self.drainPendingSettlements()
return value
})
return Effect.gen(function* () {
const result = yield* Effect.exit(evaluate)
if (Exit.isFailure(result) && Cause.hasInterruptsOnly(result.cause)) {
return yield* Effect.failCause(result.cause)
}
const drained = yield* Effect.exit(self.drainPendingSettlements())
if (Exit.isFailure(result)) return yield* Effect.failCause(result.cause)
if (Exit.isFailure(drained)) return yield* Effect.failCause(drained.cause)
return result.value
}).pipe(Effect.ensuring(Effect.sync(() => self.popScope())))
}
// Awaits every fiber-backed promise the program abandoned (fire-and-forget tool calls), so
// their work completes before the execution ends - mirroring a JS runtime waiting on
// in-flight I/O at exit. A failure nobody could have handled becomes an unhandled-rejection
// diagnostic (interrupted calls, e.g. Promise.race losers, are ignored).
// diagnostic.
private drainPendingSettlements(): Effect.Effect<void, unknown, never> {
const self = this
return Effect.gen(function* () {
let unhandled: InterpreterRuntimeError | undefined
while (self.pendingSettlements.size > 0) {
const promise = self.pendingSettlements.values().next().value
if (promise === undefined) break
const exit = yield* self.observePromise(promise)
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) continue
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || promise.handled) continue
if (unhandled !== undefined) continue
const failure = normalizeError(Cause.squash(exit.cause))
throw new InterpreterRuntimeError(
unhandled = new InterpreterRuntimeError(
`Unhandled rejection from an un-awaited promise: ${failure.message}`,
undefined,
failure.kind,
["Await promises so failures can be caught and handled."],
)
}
if (unhandled !== undefined) throw unhandled
})
}
@@ -735,7 +771,7 @@ class Interpreter<R> {
}
private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> {
return Effect.map(Effect.forkChild(effect, { startImmediately: true }), (fiber) => {
return Effect.map(Effect.forkIn(effect, this.promiseScope, { startImmediately: true }), (fiber) => {
const promise = new SandboxPromise(fiber)
this.pendingSettlements.add(promise)
return promise
@@ -752,35 +788,56 @@ class Interpreter<R> {
// `await promise`: succeed with the fulfilled value or re-raise the failure so try/catch
// observes it exactly like a synchronous throw at the await site.
private settlePromise(promise: SandboxPromise, node?: AstNode): Effect.Effect<unknown, unknown, never> {
private settlePromise(promise: SandboxPromise): Effect.Effect<unknown, unknown, never> {
const self = this
return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(promise, exit, node))
return Effect.flatMap(this.observePromise(promise), (exit) => self.unwrapPromiseExit(exit))
}
private unwrapPromiseExit(
promise: SandboxPromise | undefined,
exit: Exit.Exit<unknown, unknown>,
node?: AstNode,
): Effect.Effect<unknown, unknown> {
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
// A call Promise.race interrupted after losing settles as a catchable program failure;
// any other interruption is execution teardown (timeout/host) and must keep propagating
// as interruption rather than becoming program-visible data.
if (promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)) {
return Effect.fail(
new InterpreterRuntimeError(
"This tool call was interrupted because another value settled a Promise.race first.",
node,
private createReaction(
source: Effect.Effect<Exit.Exit<unknown, unknown>, never, R>,
reaction: (exit: Exit.Exit<unknown, unknown>, chained: SandboxPromise) => Effect.Effect<unknown, unknown, R>,
): Effect.Effect<SandboxPromise, never, R> {
const settlement = Deferred.makeUnsafe<unknown, unknown>()
const chained = new SandboxPromise(undefined, Deferred.await(settlement))
this.pendingSettlements.add(chained)
return Effect.as(
Effect.forkIn(
Effect.flatMap(source, (exit) =>
Effect.sync(() =>
Queue.offerUnsafe(this.reactionQueue, {
settlement,
effect: Effect.suspend(() => reaction(exit, chained)),
}),
),
),
)
}
this.promiseScope,
{ startImmediately: true },
),
chained,
)
}
private awaitValue(value: unknown): Effect.Effect<unknown, unknown, R> {
return Effect.flatMap(
this.createReaction(
value instanceof SandboxPromise
? Effect.exit(this.settlePromise(value))
: Effect.succeed(Exit.succeed(value)),
(exit) => this.unwrapPromiseExit(exit),
),
(continuation) => this.settlePromise(continuation),
)
}
private unwrapPromiseExit(exit: Exit.Exit<unknown, unknown>): Effect.Effect<unknown, unknown> {
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
return Effect.failCause(exit.cause)
}
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
switch (node.type) {
case "ExpressionStatement":
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" })
case "VariableDeclaration":
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
case "ReturnStatement": {
@@ -833,11 +890,6 @@ class Interpreter<R> {
const statement = asNode(statementValue, "body")
const result = yield* self.evaluateStatement(statement)
if (result.kind === "value") {
self.lastValue = result.value
continue
}
if (result.kind !== "none") {
return result
}
@@ -929,7 +981,6 @@ class Interpreter<R> {
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
if (result.kind === "return" || result.kind === "continue") return result
if (result.kind === "value") self.lastValue = result.value
}
}
return { kind: "none" } satisfies StatementResult
@@ -957,9 +1008,6 @@ class Interpreter<R> {
return result
}
if (result.kind === "value") {
self.lastValue = result.value
}
}
return { kind: "none" } satisfies StatementResult
@@ -987,9 +1035,6 @@ class Interpreter<R> {
return result
}
if (result.kind === "value") {
self.lastValue = result.value
}
} while (yield* self.evaluateExpression(testNode))
return { kind: "none" } satisfies StatementResult
@@ -1045,10 +1090,6 @@ class Interpreter<R> {
return { kind: "none" } satisfies StatementResult
}
if (result.kind === "value") {
self.lastValue = result.value
}
if (iterationScope) {
const loopScope = self.currentScope()
for (const name of perIterationBindings) {
@@ -1133,10 +1174,6 @@ class Interpreter<R> {
return { kind: "none" }
}
if (result.kind === "value") {
self.lastValue = result.value
}
if (result.kind === "continue") {
continue
}
@@ -1226,10 +1263,6 @@ class Interpreter<R> {
return { kind: "none" }
}
if (result.kind === "value") {
self.lastValue = result.value
}
if (result.kind === "continue") {
continue
}
@@ -1544,12 +1577,9 @@ class Interpreter<R> {
case "UpdateExpression":
return this.evaluateUpdateExpression(node)
case "AwaitExpression": {
// `await` resolves a promise value; awaiting anything else is a passthrough no-op,
// matching real JS semantics for non-thenables.
const self = this
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
value instanceof SandboxPromise ? self.settlePromise(value, node) : Effect.succeed(value),
)
// Even an already-settled value resumes in a later reaction turn, after work that was
// already queued, matching JavaScript's await continuation ordering.
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => this.awaitValue(value))
}
case "NewExpression":
return this.evaluateNewExpression(node)
@@ -2033,6 +2063,9 @@ class Interpreter<R> {
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
return invokeGlobalMethod(callable, args, node)
}
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
return invokeGlobalMethod(callable, args, node)
}
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
}
if (callable instanceof CoercionFunction) {
@@ -2230,14 +2263,24 @@ class Interpreter<R> {
)
}
if (ref.name === "reject") {
return Effect.sync(() => new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0]))))
return Effect.sync(() => {
const promise = new SandboxPromise(undefined, Effect.fail(new ProgramThrow(args[0])))
this.pendingSettlements.add(promise)
return promise
})
}
const items = Array.isArray(args[0]) ? args[0] : spreadItems(args[0])
// Promise combinators consume arrays through their iterator, which visits holes as
// explicit undefined values instead of preserving sparse positions like Array.map.
const items = Array.isArray(args[0]) ? [...args[0]] : spreadItems(args[0])
if (items === undefined) {
throw new InterpreterRuntimeError(
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
node,
return this.createPromise(
Effect.fail(
new InterpreterRuntimeError(
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
node,
).as("TypeError"),
),
)
}
@@ -2251,68 +2294,59 @@ class Interpreter<R> {
? Effect.map(this.observePromise(item), (exit) => ({ index, item, exit }))
: Effect.succeed({ index, item: undefined, exit: Exit.succeed(item) }),
)
return Effect.gen(function* () {
const remaining = [...observations]
const values: Array<unknown> = []
values.length = items.length
while (remaining.length > 0) {
const winner = yield* Effect.raceAll(remaining)
const position = remaining.indexOf(observations[winner.index])
if (position >= 0) remaining.splice(position, 1)
if (Exit.isSuccess(winner.exit)) {
values[winner.index] = winner.exit.value
continue
return this.createPromise(
Effect.gen(function* () {
const remaining = [...observations]
const values: Array<unknown> = []
values.length = items.length
while (remaining.length > 0) {
const winner = yield* Effect.raceAll(remaining)
const position = remaining.indexOf(observations[winner.index])
if (position >= 0) remaining.splice(position, 1)
if (Exit.isSuccess(winner.exit)) {
values[winner.index] = winner.exit.value
continue
}
for (const item of items) {
if (!(item instanceof SandboxPromise) || item === winner.item) continue
item.handled = true
self.pendingSettlements.add(item)
}
return yield* self.unwrapPromiseExit(winner.exit)
}
yield* self.createPromise(
Effect.asVoid(
Effect.forEach(
items,
(item) => (item instanceof SandboxPromise ? self.observePromise(item) : Effect.void),
{ concurrency: "unbounded" },
),
),
)
return yield* self.unwrapPromiseExit(winner.item, winner.exit, node)
}
return values
})
return values
}),
)
}
case "allSettled": {
const observations = items.map((item) =>
item instanceof SandboxPromise
? Effect.map(this.observePromise(item), (exit) => ({ promise: item as SandboxPromise | undefined, exit }))
: Effect.succeed({ promise: undefined as SandboxPromise | undefined, exit: Exit.succeed(item as unknown) }),
item instanceof SandboxPromise ? this.observePromise(item) : Effect.succeed(Exit.succeed(item as unknown)),
)
return Effect.gen(function* () {
const outcomes: Array<unknown> = []
for (const observation of observations) {
const { exit, promise } = yield* observation
if (Exit.isSuccess(exit)) {
outcomes.push(
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
)
continue
}
const raceInterrupted = promise?.interrupted === true && Cause.hasInterruptsOnly(exit.cause)
if (Cause.hasInterruptsOnly(exit.cause) && !raceInterrupted) {
// Execution teardown (timeout/host interruption), not a program-level rejection.
return yield* Effect.failCause(exit.cause)
}
const thrown = raceInterrupted
? new InterpreterRuntimeError(
"This tool call was interrupted because another value settled a Promise.race first.",
node,
return this.createPromise(
Effect.gen(function* () {
const outcomes: Array<unknown> = []
for (const observation of observations) {
const exit = yield* observation
if (Exit.isSuccess(exit)) {
outcomes.push(
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
)
: Cause.squash(exit.cause)
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(thrown),
}),
)
}
return outcomes
})
continue
}
if (Cause.hasInterruptsOnly(exit.cause)) {
// Execution teardown (timeout/host interruption), not a program-level rejection.
return yield* Effect.failCause(exit.cause)
}
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(Cause.squash(exit.cause)),
}),
)
}
return outcomes
}),
)
}
case "race": {
if (items.length === 0) {
@@ -2326,28 +2360,26 @@ class Interpreter<R> {
? Effect.map(this.observePromise(item), (exit) => ({ index, exit }))
: Effect.succeed({ index, exit: Exit.succeed(item as unknown) }),
)
return Effect.gen(function* () {
// First settlement (fulfilled OR rejected) wins; the observations never fail, so
// racing them yields exactly that. Losing in-flight calls are then interrupted.
const winner = yield* Effect.raceAll(observations)
for (const [index, item] of items.entries()) {
if (index === winner.index || !(item instanceof SandboxPromise) || item.fiber === undefined) continue
item.interrupted = true
yield* Fiber.interrupt(item.fiber)
}
const winningItem = items[winner.index]
return yield* self.unwrapPromiseExit(
winningItem instanceof SandboxPromise ? winningItem : undefined,
winner.exit,
node,
)
})
return this.createPromise(
Effect.gen(function* () {
// First settlement (fulfilled OR rejected) wins. Losers continue like native
// promises, while a supervised drain keeps their failures handled and their work
// inside the execution lifetime.
const winner = yield* Effect.raceAll(observations)
for (const [index, item] of items.entries()) {
if (index === winner.index || !(item instanceof SandboxPromise)) continue
item.handled = true
self.pendingSettlements.add(item)
}
return yield* self.unwrapPromiseExit(winner.exit)
}),
)
}
}
}
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.logs, {
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promiseScope, this.reactionQueue, this.logs, {
callPermits: this.callPermits,
pendingSettlements: this.pendingSettlements,
})
@@ -2372,7 +2404,7 @@ class Interpreter<R> {
if (fn.body.type === "BlockStatement") {
const result = yield* invocation.evaluateStatement(fn.body)
return result.kind === "return" || result.kind === "value" ? result.value : undefined
return result.kind === "return" ? result.value : undefined
}
return yield* invocation.evaluateExpression(fn.body)
@@ -2390,6 +2422,9 @@ class Interpreter<R> {
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> {
if (ref.receiver instanceof SandboxPromise) {
return this.invokePromisePrototypeMethod(ref.receiver, ref.name, args, node)
}
if (typeof ref.receiver === "string") {
if (
(ref.name === "replace" || ref.name === "replaceAll") &&
@@ -2426,6 +2461,86 @@ class Interpreter<R> {
throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
}
private invokePromisePrototypeMethod(
promise: SandboxPromise,
name: string,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<SandboxPromise, never, R> {
const callback = (value: unknown) => {
if (
!(value instanceof ToolReference && value.path.length > 0) &&
!(value instanceof PromiseMethodReference) &&
!(value instanceof CodeModeFunction) &&
!(value instanceof IntrinsicReference) &&
!(value instanceof GlobalMethodReference) &&
!(value instanceof CoercionFunction) &&
!(value instanceof UriFunction) &&
!(value instanceof ErrorConstructorReference)
)
return undefined
return (callbackArgs: Array<unknown>, chained: SandboxPromise) =>
Effect.flatMap(
value instanceof ToolReference
? this.createToolCallPromise(value.path, callbackArgs)
: value instanceof PromiseMethodReference
? this.invokePromiseMethod(value, callbackArgs, node)
: value instanceof CodeModeFunction
? this.invokeFunction(value, callbackArgs)
: value instanceof IntrinsicReference
? this.invokeIntrinsic(value, callbackArgs, node)
: value instanceof GlobalMethodReference
? Effect.sync(() => {
if (value.namespace === "console") return this.invokeConsole(value.name, callbackArgs, node)
if (
value.namespace === "Object" &&
callbackArgs[0] instanceof ToolReference &&
!objectMethodsPreservingIdentity.has(value.name)
)
return this.invokeObjectMethodOnTools(value.name, callbackArgs[0], node)
return invokeGlobalMethod(value, callbackArgs, node)
})
: value instanceof CoercionFunction
? Effect.succeed(invokeCoercion(value, callbackArgs, node))
: value instanceof UriFunction
? Effect.succeed(invokeUriFunction(value, callbackArgs, node))
: Effect.succeed(
createErrorValue(
value.name,
callbackArgs[0] === undefined ? "" : coerceToString(callbackArgs[0]),
),
),
(result) => {
if (result === chained) {
return Effect.fail(
new InterpreterRuntimeError("Chaining cycle detected for promise.", node).as("TypeError"),
)
}
return result instanceof SandboxPromise ? this.settlePromise(result) : Effect.succeed(result)
},
)
}
const onFulfilled = name === "then" ? callback(args[0]) : undefined
const onRejected = name === "then" ? callback(args[1]) : name === "catch" ? callback(args[0]) : undefined
const onFinally = name === "finally" ? callback(args[0]) : undefined
const settlement = Effect.exit(this.settlePromise(promise))
return this.createReaction(
settlement,
(exit, chained) =>
Effect.gen(function* () {
if (onFinally !== undefined) yield* onFinally([], chained)
if (Exit.isSuccess(exit)) {
if (onFulfilled === undefined) return exit.value
return yield* onFulfilled([exit.value], chained)
}
if (onRejected !== undefined)
return yield* onRejected([caughtErrorValue(Cause.squash(exit.cause))], chained)
return yield* Effect.failCause(exit.cause)
}),
)
}
private invokeStringReplacer(
value: string,
name: "replace" | "replaceAll",
@@ -3219,17 +3334,9 @@ class Interpreter<R> {
return new ComputedValue(undefined)
}
// Any property access on a promise is a confused program (`p.then(...)`, `p.value`);
// reading `undefined` here would hide the missing await, so both paths get an explicit,
// await-hinting error instead of the forgiving unknown-property fallthrough.
if (objectValue instanceof SandboxPromise) {
if (key === "then" || key === "catch" || key === "finally") {
throw new InterpreterRuntimeError(
`Promise.prototype.${String(key)} is not supported in CodeMode; use await instead (with try/catch to handle failures) - e.g. \`const result = await tools.ns.tool(...)\`.`,
propertyNode,
"UnsupportedSyntax",
[supportedSyntaxMessage],
)
return new IntrinsicReference(objectValue, key)
}
throw new InterpreterRuntimeError(
"This value is an un-awaited Promise and has no readable properties; await it first - e.g. `const result = await tools.ns.tool(...)`.",
@@ -3518,18 +3625,33 @@ export const executeWithLimits = <const Tools extends Record<string, unknown>>(
})
}
const operation = Effect.gen(function* () {
const program = parseProgram(options.code)
const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.keys, logs)
const value = yield* interpreter.run(program)
const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
return {
ok: true,
value: result,
...logged(),
toolCalls: tools.calls,
} satisfies Result
}).pipe((program) => {
const operation = Effect.scoped(
Effect.gen(function* () {
const scope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit))
const reactionQueue = yield* Queue.unbounded<PromiseReaction<Services<Tools>>>()
yield* Effect.forever(
Effect.gen(function* () {
const reaction = yield* Queue.take(reactionQueue)
yield* Effect.yieldNow
yield* Effect.forkIn(
Effect.flatMap(Effect.exit(reaction.effect), (exit) => Deferred.done(reaction.settlement, exit)),
scope,
{ startImmediately: true },
)
}),
).pipe(Effect.forkIn(scope, { startImmediately: true }))
const program = parseProgram(options.code)
const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.keys, scope, reactionQueue, logs)
const value = yield* interpreter.run(program)
const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
return {
ok: true,
value: result,
...logged(),
toolCalls: tools.calls,
} satisfies Result
}),
).pipe((program) => {
const timeoutMs = limits.timeoutMs
if (timeoutMs === undefined) return program
return program.pipe(
+2
View File
@@ -1,6 +1,7 @@
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
export const mathMethods = new Set([
"random",
"max",
"min",
"abs",
@@ -40,6 +41,7 @@ export const mathMethods = new Set([
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
if (name === "random") return Math.random()
const nums = args.map((arg) => {
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
return arg
+14 -2
View File
@@ -1,6 +1,15 @@
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
export const numberConstants = new Set([
"MAX_SAFE_INTEGER",
"MIN_SAFE_INTEGER",
"MAX_VALUE",
"MIN_VALUE",
"EPSILON",
"NaN",
"POSITIVE_INFINITY",
"NEGATIVE_INFINITY",
])
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
@@ -32,6 +41,9 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
result = value.toString(radix)
break
}
case "valueOf":
result = value
break
default:
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
}
+18 -15
View File
@@ -1,6 +1,6 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember } from "../tool-runtime.js"
import { isSandboxValue, SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js"
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
@@ -10,13 +10,23 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
const requireObject = (): Record<string, unknown> => {
const input = args[0]
const value = boundedData(args[0], `Object.${name} input`)
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
if (isSandboxValue(value)) return {}
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node)
if (isSandboxValue(input)) return {}
if (input instanceof SandboxPromise) {
throw new InterpreterRuntimeError(
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
node,
"InvalidDataValue",
)
}
return value as Record<string, unknown>
if (input === null || typeof input !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
const prototype = Object.getPrototypeOf(input)
if (prototype !== null && prototype !== Object.prototype) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
return input as Record<string, unknown>
}
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
@@ -28,15 +38,8 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
guardedSet(out, coerceToString(key), item)
}
switch (name) {
case "keys": {
const value = boundedData(args[0], "Object.keys input")
if (isSandboxValue(value)) return []
if (Array.isArray(value)) return Object.keys(value)
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
}
return Object.keys(value)
}
case "keys":
return Object.keys(requireObject())
case "values":
return Object.values(requireObject())
case "entries":
+2 -1
View File
@@ -616,7 +616,8 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
"## Language",
"",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
]
+1 -1
View File
@@ -1,7 +1,7 @@
import type { Effect, Fiber } from "effect"
export class SandboxPromise {
interrupted = false
handled = false
constructor(
readonly fiber: Fiber.Fiber<unknown, unknown> | undefined,
readonly immediate?: Effect.Effect<unknown, unknown>,
+28
View File
@@ -0,0 +1,28 @@
Test262: ECMAScript Test Suite ("Software") is protected by copyright and is being
made available under the "BSD License", included below. This Software may be subject to third party rights (rights
from parties other than Ecma International), including patent rights, and no licenses under such third party rights
are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA
CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://www.ecma-international.org/ipr FOR
INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS*.
Copyright (C) 2012 Ecma International
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
* Ecma International Standards hereafter means Ecma International Standards as well as Ecma Technical Reports
@@ -0,0 +1,325 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/Array/prototype/map/15.4.4.19-8-1.js
* - test/built-ins/Array/prototype/map/15.4.4.19-8-2.js
* - test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js
* - test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js
* - test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js
* - test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js
* - test/built-ins/Array/prototype/find/predicate-call-parameters.js
* - test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js
* - test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js
* - test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js
* - test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js
* - test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js
* - test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js
* - test/built-ins/Array/prototype/findLast/predicate-call-parameters.js
* - test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js
* - test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js
* - test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js
* - test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js
* - test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js
* - test/built-ins/Array/prototype/some/15.4.4.17-7-1.js
* - test/built-ins/Array/prototype/some/15.4.4.17-8-1.js
* - test/built-ins/Array/prototype/every/15.4.4.16-7-1.js
* - test/built-ins/Array/prototype/every/15.4.4.16-8-1.js
* - test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js
* - test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js
* - test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js
* - test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js
* - test/built-ins/Array/prototype/flatMap/depth-always-one.js
* - test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js
* - test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js
* - test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js
* - test/built-ins/Array/prototype/sort/stability-5-elements.js
* - test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js
* - test/built-ins/Array/prototype/toSorted/comparefn-default.js
* - test/built-ins/Array/prototype/toSorted/immutable.js
* - test/built-ins/Array/prototype/toSorted/zero-or-one-element.js
*
* Copyright (C) 2015 the V8 project authors. All rights reserved.
* Copyright (C) 2018 Mathias Bynens. All rights reserved.
* Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved.
* Copyright (C) 2021 Igalia, S.L. All rights reserved.
* Copyright (C) 2021 Microsoft. All rights reserved.
* Copyright (C) 2025 Google. All rights reserved.
* Copyright (C) 2026 Garham Lee. All rights reserved.
* Copyright (c) 2012 Ecma International. All rights reserved.
* Copyright 2009 the Sputnik authors. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const cases = [
{
path: "test/built-ins/Array/prototype/map/15.4.4.19-8-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.map((value) => { input[2] = 3; input[5] = 6; return 1 }); return [result.length, result[5] === undefined]`,
expected: [5, true],
},
{
path: "test/built-ins/Array/prototype/map/15.4.4.19-8-2.js",
code: `const input = [1, 2, 3, 4, 5]; const result = input.map((value) => { input[4] = -1; return value > 0 ? 1 : 0 }); return [result.length, result[4]]`,
expected: [5, 0],
},
{
path: "test/built-ins/Array/prototype/map/15.4.4.19-8-b-1.js",
code: `const input = []; input[10] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.map(() => { calls += 1; return 1 }); return [result.length, calls, 0 in result, 1 in result]`,
expected: [10, 1, false, true],
},
{
path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const result = input.filter(() => { input[2] = 3; input[5] = 6; return true }); return result`,
expected: [1, 2, 3, 4, 5],
},
{
path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-2.js",
code: `const input = [1, 2, 3, 4, 5]; return input.filter((value) => { input[2] = -1; input[4] = -1; return value > 0 })`,
expected: [1, 2, 4],
},
{
path: "test/built-ins/Array/prototype/filter/15.4.4.20-9-b-1.js",
code: `const input = []; input[9] = 0; input.pop(); input[1] = undefined; let calls = 0; const result = input.filter(() => { calls += 1; return false }); return [result, calls]`,
expected: [[], 1],
},
{
path: "test/built-ins/Array/prototype/find/predicate-call-parameters.js",
code: `const input = [10, 20]; const seen = []; input.find((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`,
expected: [
[10, 0, true],
[20, 1, true],
],
},
{
path: "test/built-ins/Array/prototype/find/return-found-value-predicate-result-is-true.js",
code: `return [1, 2, 3].find((value) => value > 1)`,
expected: 2,
},
{
path: "test/built-ins/Array/prototype/find/return-undefined-if-predicate-returns-false-value.js",
code: `return [1, 2, 3].find((value) => value > 4) === undefined`,
expected: true,
},
{
path: "test/built-ins/Array/prototype/find/predicate-not-called-on-empty-array.js",
code: `let calls = 0; const result = [].find(() => { calls += 1; return true }); return [result === undefined, calls]`,
expected: [true, 0],
},
{
path: "test/built-ins/Array/prototype/findIndex/predicate-call-parameters.js",
code: `const input = [10, 20]; const seen = []; input.findIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`,
expected: [
[10, 0, true],
[20, 1, true],
],
},
{
path: "test/built-ins/Array/prototype/findIndex/return-index-predicate-result-is-true.js",
code: `return [1, 2, 3].findIndex((value) => value > 1)`,
expected: 1,
},
{
path: "test/built-ins/Array/prototype/findIndex/return-negative-one-if-predicate-returns-false-value.js",
code: `return [1, 2, 3].findIndex((value) => value > 4)`,
expected: -1,
},
{
path: "test/built-ins/Array/prototype/findLast/predicate-call-parameters.js",
code: `const input = [10, 20]; const seen = []; input.findLast((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`,
expected: [
[20, 1, true],
[10, 0, true],
],
},
{
path: "test/built-ins/Array/prototype/findLast/return-found-value-predicate-result-is-true.js",
code: `return [1, 2, 3].findLast((value) => value < 3)`,
expected: 2,
},
{
path: "test/built-ins/Array/prototype/findLast/return-undefined-if-predicate-returns-false-value.js",
code: `return [1, 2, 3].findLast((value) => value > 4) === undefined`,
expected: true,
},
{
path: "test/built-ins/Array/prototype/findLastIndex/predicate-call-parameters.js",
code: `const input = [10, 20]; const seen = []; input.findLastIndex((value, index, receiver) => { seen.push([value, index, receiver === input]); return false }); return seen`,
expected: [
[20, 1, true],
[10, 0, true],
],
},
{
path: "test/built-ins/Array/prototype/findLastIndex/return-index-predicate-result-is-true.js",
code: `return [1, 2, 3].findLastIndex((value) => value < 3)`,
expected: 1,
},
{
path: "test/built-ins/Array/prototype/findLastIndex/return-negative-one-if-predicate-returns-false-value.js",
code: `return [1, 2, 3].findLastIndex((value) => value > 4)`,
expected: -1,
},
{
path: "test/built-ins/Array/prototype/some/15.4.4.17-7-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.some((value) => { input[2] = 3; seen.push(value); return false }); return [result, seen.includes(3)]`,
expected: [false, true],
},
{
path: "test/built-ins/Array/prototype/some/15.4.4.17-8-1.js",
code: `return [].some(() => true)`,
expected: false,
},
{
path: "test/built-ins/Array/prototype/every/15.4.4.16-7-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = 5; const seen = []; const result = input.every((value) => { input[2] = 3; seen.push(value); return true }); return [result, seen.includes(3)]`,
expected: [true, true],
},
{
path: "test/built-ins/Array/prototype/every/15.4.4.16-8-1.js",
code: `return [].every(() => false)`,
expected: true,
},
{
path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = 5; let calls = 0; input.forEach(() => { calls += 1; input[2] = 3; input[5] = 6 }); return calls`,
expected: 5,
},
{
path: "test/built-ins/Array/prototype/forEach/15.4.4.18-7-2.js",
code: `const input = [1, 2, 3]; const seen = []; input.forEach((value, index) => { seen.push(value); if (index === 0) input.pop() }); return seen`,
expected: [1, 2],
},
{
path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-1.js",
code: `const input = [1, 2]; input[3] = 4; input[4] = "5"; return input.reduce((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`,
expected: "105",
},
{
path: "test/built-ins/Array/prototype/reduce/15.4.4.21-9-5.js",
code: `let calls = 0; const result = [1].reduce(() => { calls += 1; return 2 }); return [result, calls]`,
expected: [1, 0],
},
{
path: "test/built-ins/Array/prototype/reduce/15.4.4.21-10-1.js",
code: `const input = [1, 2, 3, 4, 5]; input.reduce(() => 1); return input`,
expected: [1, 2, 3, 4, 5],
},
{
path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-1.js",
code: `const input = ["1", 2]; input[3] = 4; input[4] = "5"; return input.reduceRight((accumulator, value) => { input[2] = 3; input[5] = 6; return accumulator + value })`,
expected: "54321",
},
{
path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-9-5.js",
code: `let calls = 0; const result = [1].reduceRight(() => { calls += 1; return 2 }); return [result, calls]`,
expected: [1, 0],
},
{
path: "test/built-ins/Array/prototype/reduceRight/15.4.4.22-10-1.js",
code: `const input = [1, 2, 3, 4, 5]; input.reduceRight(() => 1); return input`,
expected: [1, 2, 3, 4, 5],
},
{
path: "test/built-ins/Array/prototype/flatMap/depth-always-one.js",
code: `return [1, 2, 3].flatMap((value) => [[value * 2]])`,
expected: [[2], [4], [6]],
},
{
path: "test/built-ins/Array/prototype/flatMap/mapperfunction-throws.js",
code: `try { [1, 2].flatMap(() => { throw "stop" }) } catch (error) { return error === "stop" } return false`,
expected: true,
},
{
path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A1.1_T1.js",
code: `const input = []; input[2] = 0; input.pop(); input.sort(); return [input.length, input[0] === undefined, input[1] === undefined]`,
expected: [2, true, true],
},
{
path: "test/built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T1.js",
code: `return ["z", "y", "x", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n", "M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"].sort()`,
expected: [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
],
},
{
path: "test/built-ins/Array/prototype/sort/stability-5-elements.js",
code: `const input = [{ n: "A", r: 2 }, { n: "B", r: 3 }, { n: "C", r: 2 }, { n: "D", r: 3 }, { n: "E", r: 3 }]; return input.sort((left, right) => right.r - left.r).map((item) => item.n).join("")`,
expected: "BDEAC",
},
{
path: "test/built-ins/Array/prototype/toSorted/comparefn-controls-sort.js",
code: `const mixed = [333, 33, 3, 222, 22, 2, 111, 11, 1]; return [[1, 2, 3, 4].toSorted((a, b) => a - b), [4, 3, 2, 1].toSorted((a, b) => a - b), mixed.toSorted((a, b) => a - b), [1, 2, 3, 4].toSorted((a, b) => b - a), [4, 3, 2, 1].toSorted((a, b) => b - a), mixed.toSorted((a, b) => b - a)]`,
expected: [
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 11, 22, 33, 111, 222, 333],
[4, 3, 2, 1],
[4, 3, 2, 1],
[333, 222, 111, 33, 22, 11, 3, 2, 1],
],
},
{
path: "test/built-ins/Array/prototype/toSorted/comparefn-default.js",
code: `return [[1, 2, 3, 4].toSorted(), [4, 3, 2, 1].toSorted(), ["a", 2, 1, "z"].toSorted(), [333, 33, 3, 222, 22, 2, 111, 11, 1].toSorted()]`,
expected: [
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, "a", "z"],
[1, 11, 111, 2, 22, 222, 3, 33, 333],
],
},
{
path: "test/built-ins/Array/prototype/toSorted/immutable.js",
code: `const input = [2, 0, 1]; const result = input.toSorted(); return [input, result !== input]`,
expected: [[2, 0, 1], true],
},
{
path: "test/built-ins/Array/prototype/toSorted/zero-or-one-element.js",
code: `const zero = []; const one = [1]; const zeroResult = zero.toSorted(); const oneResult = one.toSorted(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`,
expected: [[], [1], true, true],
},
] as const
describe("Test262 Array callback adaptations", () => {
for (const item of cases) {
test(item.path, async () => {
expect(await value(item.code)).toEqual(item.expected)
})
}
})
@@ -0,0 +1,323 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/Array/prototype/includes/samevaluezero.js
* - test/built-ins/Array/prototype/includes/using-fromindex.js
* - test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js
* - test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js
* - test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js
* - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js
* - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js
* - test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js
* - test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js
* - test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js
* - test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js
* - test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js
* - test/built-ins/Array/prototype/at/returns-item.js
* - test/built-ins/Array/prototype/at/returns-item-relative-index.js
* - test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js
* - test/built-ins/Array/prototype/flat/null-undefined-elements.js
* - test/built-ins/Array/prototype/flat/positive-infinity.js
* - test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js
* - test/built-ins/Array/prototype/toReversed/immutable.js
* - test/built-ins/Array/prototype/toReversed/zero-or-one-element.js
* - test/built-ins/Array/prototype/with/immutable.js
* - test/built-ins/Array/prototype/with/index-negative.js
* - test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js
* - test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js
* - test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js
* - test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js
* - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js
* - test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js
* - test/built-ins/Array/prototype/splice/called_with_one_argument.js
* - test/built-ins/Array/prototype/fill/fill-values.js
* - test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js
* - test/built-ins/Array/prototype/fill/return-this.js
* - test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js
* - test/built-ins/Array/prototype/copyWithin/return-this.js
* - test/built-ins/Array/prototype/keys/iteration.js
* - test/built-ins/Array/prototype/values/iteration.js
* - test/built-ins/Array/prototype/entries/iteration.js
* - test/built-ins/Array/isArray/15.4.3.2-0-3.js
* - test/built-ins/Array/isArray/15.4.3.2-0-4.js
* - test/built-ins/Array/from/from-array.js
* - test/built-ins/Array/from/from-string.js
* - test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js
* - test/built-ins/Array/of/creates-a-new-array-from-arguments.js
*
* Copyright (C) 2015 André Bargull. All rights reserved.
* Copyright (C) 2015 the V8 project authors. All rights reserved.
* Copyright (C) 2016 the V8 project authors. All rights reserved.
* Copyright (C) 2018 Shilpi Jain and Michael Ficarra. All rights reserved.
* Copyright (C) 2020 Alexey Shvayka. All rights reserved.
* Copyright (C) 2020 Rick Waldron. All rights reserved.
* Copyright (C) 2021 Igalia, S.L. All rights reserved.
* Copyright (c) 2012 Ecma International. All rights reserved.
* Copyright (c) 2014 Hank Yates. All rights reserved.
* Copyright (c) 2015 the V8 project authors. All rights reserved.
* Copyright (c) 2021 Rick Waldron. All rights reserved.
* Copyright 2009 the Sputnik authors. All rights reserved.
* Copyright 2015 Microsoft Corporation. All rights reserved.
* Copyright 2016 The V8 project authors. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const cases = [
{
path: "test/built-ins/Array/prototype/includes/samevaluezero.js",
code: `const input = [42, 0, 1, NaN]; return [input.includes(42), input.includes("42"), input.includes([42]), input.includes(true), input.includes(NaN), input.includes(0), input.includes(-0), input.includes(null), input.includes("")]`,
expected: [true, false, false, false, true, true, true, false, false],
},
{
path: "test/built-ins/Array/prototype/includes/using-fromindex.js",
code: `const input = ["a", "b", "c"]; return [input.includes("a", 0), input.includes("a", 1), input.includes("a", -4), input.includes("a", -3), input.includes("a", -2), input.includes("b", 0), input.includes("b", 1), input.includes("b", 2), input.includes("b", -3), input.includes("b", -2), input.includes("b", -1), input.includes("c", 0), input.includes("c", 2), input.includes("c", 3), input.includes("c", -3), input.includes("c", -1)]`,
expected: [true, false, true, true, false, true, true, false, true, true, false, true, true, false, true, true],
},
{
path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js",
code: `return [[0, 1, 2, 3].join("&"), [0, 1, 2, 3].join("")]`,
expected: ["0&1&2&3", "0123"],
},
{
path: "test/built-ins/Array/prototype/join/S15.4.4.5_A3.2_T1.js",
code: `return [
["", "", ""].join(""),
["&", "&", "&"].join("&"),
[true, true, true].join(),
[null, null, null].join(),
[undefined, undefined, undefined].join(),
[Infinity, Infinity, Infinity].join(),
[NaN, NaN, NaN].join(),
]`,
expected: ["", "&&&&&", "true,true,true", ",,", ",,", "Infinity,Infinity,Infinity", "NaN,NaN,NaN"],
},
{
path: "test/built-ins/Array/prototype/slice/S15.4.4.10_A1.2_T2.js",
code: `return [0, 1, 2, 3, 4].slice(-1, 5)`,
expected: [4],
},
{
path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T1.js",
code: `return [].concat([0, 1], [2, 3, 4])`,
expected: [0, 1, 2, 3, 4],
},
{
path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T2.js",
code: `const object = { value: 1 }; const result = [0].concat(object, [1, 2], -1, true, "NaN"); return [result, result[1] === object]`,
expected: [[0, { value: 1 }, 1, 2, -1, true, "NaN"], true],
},
{
path: "test/built-ins/Array/prototype/concat/S15.4.4.4_A1_T3.js",
code: `const input = [0, 1]; const result = input.concat(); return [result, result !== input]`,
expected: [[0, 1], true],
},
{
path: "test/built-ins/Array/prototype/indexOf/fromindex-zero-conversion.js",
code: `const result = [true].indexOf(true, -0); return [result, 1 / result === Infinity]`,
expected: [0, true],
},
{
path: "test/built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js",
code: `return [].indexOf(1)`,
expected: -1,
},
{
path: "test/built-ins/Array/prototype/lastIndexOf/fromindex-zero-conversion.js",
code: `const result = [true].lastIndexOf(true, -0); return [result, 1 / result === Infinity]`,
expected: [0, true],
},
{
path: "test/built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js",
code: `return [].lastIndexOf(1)`,
expected: -1,
},
{
path: "test/built-ins/Array/prototype/at/returns-item.js",
code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(1), input.at(2), input.at(3), input.at(4) === undefined, input.at(5)]`,
expected: [1, 2, 3, 4, true, 5],
},
{
path: "test/built-ins/Array/prototype/at/returns-item-relative-index.js",
code: `const input = [1, 2, 3, 4, undefined, 5]; return [input.at(0), input.at(-1), input.at(-2) === undefined, input.at(-3), input.at(-4)]`,
expected: [1, 5, true, 4, 3],
},
{
path: "test/built-ins/Array/prototype/at/returns-undefined-for-out-of-range-index.js",
code: `const input = []; return [input.at(-2) === undefined, input.at(0) === undefined, input.at(1) === undefined]`,
expected: [true, true, true],
},
{
path: "test/built-ins/Array/prototype/flat/null-undefined-elements.js",
code: `const result = [1, [null, [undefined]]].flat(2); return [result.length, result[0], result[1] === null, result[2] === undefined]`,
expected: [3, 1, true, true],
},
{
path: "test/built-ins/Array/prototype/flat/positive-infinity.js",
code: `return [1, [2, [3, [4]]]].flat(Infinity)`,
expected: [1, 2, 3, 4],
},
{
path: "test/built-ins/Array/prototype/reverse/S15.4.4.8_A1_T1.js",
code: `const empty = []; const one = [1]; const input = [1, 2]; const emptyResult = empty.reverse(); const oneResult = one.reverse(); const result = input.reverse(); return [emptyResult === empty, oneResult === one, result === input, input]`,
expected: [true, true, true, [2, 1]],
},
{
path: "test/built-ins/Array/prototype/toReversed/immutable.js",
code: `const input = [0, 1, 2]; const result = input.toReversed(); return [input, result !== input]`,
expected: [[0, 1, 2], true],
},
{
path: "test/built-ins/Array/prototype/toReversed/zero-or-one-element.js",
code: `const zero = []; const one = [1]; const zeroResult = zero.toReversed(); const oneResult = one.toReversed(); return [zeroResult, oneResult, zeroResult !== zero, oneResult !== one]`,
expected: [[], [1], true, true],
},
{
path: "test/built-ins/Array/prototype/with/immutable.js",
code: `const input = [0, 1, 2]; const result = input.with(1, 3); return [input, result !== input, input.with(1, 1) !== input]`,
expected: [[0, 1, 2], true, true],
},
{
path: "test/built-ins/Array/prototype/with/index-negative.js",
code: `const input = [0, 1, 2]; return [input.with(-1, 4), input.with(-3, 4)]`,
expected: [
[0, 1, 4],
[4, 1, 2],
],
},
{
path: "test/built-ins/Array/prototype/push/S15.4.4.7_A1_T1.js",
code: `const input = []; return [input.push(1), input.push(), input.push(-1), input]`,
expected: [1, 1, 2, [1, -1]],
},
{
path: "test/built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js",
code: `const input = []; return [input.pop() === undefined, input.length]`,
expected: [true, 0],
},
{
path: "test/built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js",
code: `const input = []; return [input.shift() === undefined, input.length]`,
expected: [true, 0],
},
{
path: "test/built-ins/Array/prototype/unshift/S15.4.4.13_A1_T1.js",
code: `const input = []; return [input.unshift(1), input[0], input.unshift(), input.unshift(-1), input]`,
expected: [1, 1, 1, 2, [-1, 1]],
},
{
path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.1_T1.js",
code: `const input = [0, 1, 2, 3]; const removed = input.splice(0, 3); return [input, removed]`,
expected: [[3], [0, 1, 2]],
},
{
path: "test/built-ins/Array/prototype/splice/S15.4.4.12_A1.2_T1.js",
code: `const input = [0, 1]; const removed = input.splice(-2, -1); return [input, removed]`,
expected: [[0, 1], []],
},
{
path: "test/built-ins/Array/prototype/splice/called_with_one_argument.js",
code: `const input = ["first", "second", "third"]; const removed = input.splice(1); return [input, removed]`,
expected: [["first"], ["second", "third"]],
},
{
path: "test/built-ins/Array/prototype/fill/fill-values-custom-start-and-end.js",
code: `const input = [0, 0, 0, 0, 0]; input.fill(8, -3, 4); const sparse = []; sparse[4] = 0; sparse.fill(8, 1, 3); return [[0, 0, 0].fill(8, 1, 2), input, [0, 0, 0, 0, 0].fill(8, -2, -1), [0, 0, 0, 0, 0].fill(8, -1, -3), [0 in sparse, sparse[1], sparse[2], 3 in sparse, sparse[4]]]`,
expected: [
[0, 8, 0],
[0, 0, 8, 8, 0],
[0, 0, 0, 8, 0],
[0, 0, 0, 0, 0],
[false, 8, 8, false, 0],
],
},
{
path: "test/built-ins/Array/prototype/fill/return-this.js",
code: `const input = []; return input.fill(1) === input`,
expected: true,
},
{
path: "test/built-ins/Array/prototype/fill/fill-values.js",
code: `const omitted = [0, 0].fill(); return [[].fill(8), omitted.map((value) => value === undefined), [0, 0, 0].fill(8)]`,
expected: [[], [true, true], [8, 8, 8]],
},
{
path: "test/built-ins/Array/prototype/copyWithin/non-negative-target-start-and-end.js",
code: `return [[0, 1, 2, 3].copyWithin(0, 0, 0), [0, 1, 2, 3].copyWithin(0, 0, 2), [0, 1, 2, 3].copyWithin(0, 1, 2), [0, 1, 2, 3].copyWithin(1, 0, 2), [0, 1, 2, 3, 4, 5].copyWithin(1, 3, 5)]`,
expected: [
[0, 1, 2, 3],
[0, 1, 2, 3],
[1, 1, 2, 3],
[0, 0, 1, 3],
[0, 3, 4, 3, 4, 5],
],
},
{
path: "test/built-ins/Array/prototype/copyWithin/return-this.js",
code: `const input = [0, 1, 2, 3]; const result = input.copyWithin(1, 0, 2); return [input, result === input]`,
expected: [[0, 0, 1, 3], true],
},
{
path: "test/built-ins/Array/prototype/keys/iteration.js",
code: `return ["a", "b", "c"].keys()`,
expected: [0, 1, 2],
},
{
path: "test/built-ins/Array/prototype/values/iteration.js",
code: `return ["a", "b", "c"].values()`,
expected: ["a", "b", "c"],
},
{
path: "test/built-ins/Array/prototype/entries/iteration.js",
code: `return ["a", "b"].entries()`,
expected: [
[0, "a"],
[1, "b"],
],
},
{
path: "test/built-ins/Array/isArray/15.4.3.2-0-3.js",
code: `return [Array.isArray([]), Array.isArray([1]), Array.isArray(Array.of(1))]`,
expected: [true, true, true],
},
{
path: "test/built-ins/Array/isArray/15.4.3.2-0-4.js",
code: `return [Array.isArray(42), Array.isArray({}), Array.isArray(null), Array.isArray("array")]`,
expected: [false, false, false, false],
},
{
path: "test/built-ins/Array/from/from-array.js",
code: `const input = [0, "foo", undefined, Infinity]; const result = Array.from(input); return [result.length, result[0], result[1], result[2] === undefined, result[3] === Infinity, result !== input, result instanceof Array]`,
expected: [4, 0, "foo", true, true, true, true],
},
{
path: "test/built-ins/Array/from/from-string.js",
code: `return Array.from("Test")`,
expected: ["T", "e", "s", "t"],
},
{
path: "test/built-ins/Array/from/array-like-has-length-but-no-indexes-with-values.js",
code: `const result = Array.from({ length: 5 }); const mapped = result.map(() => 1); return [result.length, result.map((value) => value === undefined), mapped.length, mapped]`,
expected: [5, [true, true, true, true, true], 5, [1, 1, 1, 1, 1]],
},
{
path: "test/built-ins/Array/of/creates-a-new-array-from-arguments.js",
code: `const mixed = Array.of(undefined, false, null, undefined); return [Array.of("Mike", "Rick", "Leo"), mixed.length, mixed[0] === undefined, mixed[1], mixed[2], mixed[3] === undefined, Array.of()]`,
expected: [["Mike", "Rick", "Leo"], 4, true, false, null, true, []],
},
] as const
describe("Test262 Array core adaptations", () => {
for (const item of cases) {
test(item.path, async () => {
expect(await value(item.code)).toEqual(item.expected)
})
}
})
+25 -1
View File
@@ -652,12 +652,18 @@ describe("CodeMode public contract", () => {
expect(instructions).toContain("not a general-purpose runtime")
expect(instructions).not.toContain("Standard modern JavaScript works")
expect(instructions).not.toContain("TypeScript type annotations")
for (const missing of ["Modules/imports", "classes", "generators", "fetch", "promise chaining"]) {
for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
expect(instructions).toContain(missing)
}
expect(instructions).toContain("selected standard-library methods, and awaited tool calls")
expect(instructions).toContain("Use await with try/catch")
expect(instructions).not.toContain("promise chaining are unavailable")
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
expect(instructions).not.toContain("host globals")
expect(instructions).toContain("Use Code Mode tools for external operations")
expect(instructions).toContain(
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
)
expect(instructions).toContain(
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
)
@@ -1081,6 +1087,24 @@ describe("CodeMode public contract", () => {
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
})
test("returns the final top-level expression when return is omitted", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` }))
expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] })
})
test("does not implicitly return expressions nested in control flow", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` }))
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
})
test("returns null when the final top-level statement is not an expression", async () => {
const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` }))
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
})
test("rejects invalid configuration and discovery limits", async () => {
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => {
const spec = await opencodeSpec()
const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(result.skipped).toHaveLength(5)
expect(result.skipped).toHaveLength(4)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/pty/{ptyID}/connect",
reason: "WebSocket operations are not supported",
})
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3)
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/fs/read/*",
@@ -210,11 +210,11 @@ describe("OpenAPI.fromSpec", () => {
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined()
})
test("preserves operation path sanitization and collision handling", () => {
+9 -87
View File
@@ -42,11 +42,6 @@ describe("H2: string property access reads as undefined (not a throw)", () => {
test("unknown property on a number is undefined", async () => {
expect(await value(`return (5).foo ?? "n"`)).toBe("n")
})
test("supported string methods still work", async () => {
expect(await value(`return "AB".toLowerCase()`)).toBe("ab")
expect(await value(`return "hello".length`)).toBe(5)
})
})
describe("H3: array property access reads as undefined (not a throw)", () => {
@@ -63,8 +58,7 @@ describe("H3: array property access reads as undefined (not a throw)", () => {
expect(await value(`return [1,2,3].toSpliced === undefined`)).toBe(true)
})
test("supported array methods and indexing still work", async () => {
expect(await value(`return [1,2,3].map(x => x + 1)`)).toEqual([2, 3, 4])
test("array indexing still works", async () => {
expect(await value(`return [1,2,3][9] === undefined`)).toBe(true)
expect(await value(`return [1,2,3][9]`)).toBeNull()
})
@@ -202,9 +196,6 @@ describe("Error values and instanceof", () => {
"TypeError",
true,
])
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual(
["RangeError", true],
)
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
"SyntaxError",
true,
@@ -263,55 +254,18 @@ describe("Error values and instanceof", () => {
})
})
describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
test("sort and reverse mutate and return the receiver", async () => {
describe("CodeMode-specific array behavior", () => {
test("sort with a comparator mutates and returns the receiver", async () => {
expect(
await value(`
const sorted = [3, 1, 2]
const sortResult = sorted.sort((a, b) => a - b)
const reversed = [1, 2, 3]
const reverseResult = reversed.reverse()
return { sorted, sameSort: sorted === sortResult, reversed, sameReverse: reversed === reverseResult }
const input = [3, 1, 2]
const result = input.sort((a, b) => a - b)
return { input, same: input === result }
`),
).toEqual({ sorted: [1, 2, 3], sameSort: true, reversed: [3, 2, 1], sameReverse: true })
).toEqual({ input: [1, 2, 3], same: true })
})
test("array callbacks receive the receiver and observe later mutations", async () => {
expect(
await value(`
const values = [1, 2, 3]
const seen = values.map((value, index, receiver) => {
if (index === 0) values[1] = 9
return [value, receiver === values]
})
return seen
`),
).toEqual([
[1, true],
[9, true],
[3, true],
])
expect(
await value(`
const values = [1, 2, 3]
const seen = []
values.forEach((value, index) => {
seen.push(value)
if (index === 0) values.pop()
})
return seen
`),
).toEqual([1, 2])
})
test("splice removes in place and returns the removed elements", async () => {
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({
removed: [2, 3],
a: [1, 4],
})
})
test("splice inserts new elements at the cut", async () => {
test("splice can replace and insert elements", async () => {
expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"])
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({
removed: [2],
@@ -319,32 +273,12 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
})
})
test("splice with one argument removes to the end; negative start counts back", async () => {
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({
removed: [2, 3],
a: [1],
})
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({
removed: [3],
a: [1, 2],
})
})
test("splice rejects inserting a container into itself", async () => {
const err = await error(`const a = [1]; a.splice(0, 0, [a]); return a`)
expect(err.kind).toBe("InvalidDataValue")
expect(err.message).toContain("circular")
})
test("fill overwrites a range and returns the mutated array", async () => {
expect(await value(`const a = [1,2,3,4]; return a.fill(0, 1, 3)`)).toEqual([1, 0, 0, 4])
expect(await value(`return [1,2,3].fill("z")`)).toEqual(["z", "z", "z"])
})
test("copyWithin copies a range in place", async () => {
expect(await value(`return [1,2,3,4,5].copyWithin(0, 3)`)).toEqual([4, 5, 3, 4, 5])
})
test("keys/values/entries return arrays usable with for...of and spread", async () => {
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
@@ -359,16 +293,9 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
})
})
describe("string methods: localeCompare, normalize, trim aliases", () => {
describe("CodeMode-specific string behavior", () => {
test("localeCompare orders strings for sorting", async () => {
expect(await value(`return ["b","a","c"].sort((x, y) => x.localeCompare(y))`)).toEqual(["a", "b", "c"])
expect(await value(`return "a".localeCompare("a")`)).toBe(0)
})
test("normalize applies unicode normalization forms", async () => {
expect(await value(`return "\\u0065\\u0301".normalize("NFC").length`)).toBe(1)
expect(await value(`return "\\u00e9".normalize("NFD").length`)).toBe(2)
expect(await value(`return "x".normalize() === "x"`)).toBe(true)
})
test("an invalid normalize form is a clear catchable error", async () => {
@@ -453,11 +380,6 @@ describe("H5: builtin coercion functions work as array callbacks", () => {
expect(await value(`return [1, 2, 3].map(String)`)).toEqual(["1", "2", "3"])
})
test("arrow callbacks still work (no regression)", async () => {
expect(await value(`return [1, 2, 3, 4].filter(x => x % 2 === 0)`)).toEqual([2, 4])
expect(await value(`return [1, 2, 3].reduce((a, b) => a + b, 0)`)).toBe(6)
})
test("a non-callable callback is still rejected", async () => {
const err = await error(`return [1,2,3].map(42)`)
expect(err.message).toContain("callback")
@@ -0,0 +1,334 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/Promise/all/S25.4.4.1_A1.1_T1.js
* - test/built-ins/Promise/all/S25.4.4.1_A2.1_T1.js
* - test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js
* - test/built-ins/Promise/all/S25.4.4.1_A2.3_T2.js
* - test/built-ins/Promise/all/S25.4.4.1_A2.3_T3.js
* - test/built-ins/Promise/all/S25.4.4.1_A7.1_T1.js
* - test/built-ins/Promise/all/S25.4.4.1_A8.2_T1.js
* - test/built-ins/Promise/all/S25.4.4.1_A8.2_T2.js
* - test/built-ins/Promise/all/iter-arg-is-number-reject.js
* - test/built-ins/Promise/all/resolve-non-thenable.js
* - test/built-ins/Promise/allSettled/is-function.js
* - test/built-ins/Promise/allSettled/returns-promise.js
* - test/built-ins/Promise/allSettled/resolves-empty-array.js
* - test/built-ins/Promise/allSettled/resolves-to-array.js
* - test/built-ins/Promise/allSettled/resolved-all-fulfilled.js
* - test/built-ins/Promise/allSettled/resolved-all-mixed.js
* - test/built-ins/Promise/allSettled/resolved-all-rejected.js
* - test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
* - test/built-ins/Promise/allSettled/resolve-non-thenable.js
* - test/built-ins/Promise/race/S25.4.4.3_A1.1_T1.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.1_T1.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.1_T2.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.1_T3.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.2_T1.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.3_T1.js
* - test/built-ins/Promise/race/S25.4.4.3_A7.3_T2.js
* - test/built-ins/Promise/race/iter-arg-is-number-reject.js
* - test/built-ins/Promise/resolve/S25.4.4.5_A1.1_T1.js
* - test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js
* - test/built-ins/Promise/resolve/resolve-non-obj.js
* - test/built-ins/Promise/resolve/resolve-non-thenable.js
* - test/built-ins/Promise/reject/S25.4.4.4_A1.1_T1.js
* - test/built-ins/Promise/reject/S25.4.4.4_A2.1_T1.js
* - test/built-ins/Array/from/from-array.js
* - test/language/statements/async-function/declaration-returns-promise.js
* - test/language/statements/async-function/evaluation-body-that-returns.js
* - test/language/statements/async-function/evaluation-body-that-returns-after-await.js
* - test/language/statements/async-function/evaluation-body-that-throws.js
* - test/language/statements/async-function/evaluation-body-that-throws-after-await.js
*
* Copyright 2014 Cubane Canada, Inc. All rights reserved.
* Copyright (C) 2016 the V8 project authors. All rights reserved.
* Copyright (C) 2019 Leo Balter. All rights reserved.
* Copyright (C) 2018 Rick Waldron. All rights reserved.
* Copyright 2015 Microsoft Corporation. All rights reserved.
* Copyright 2016 Microsoft, Inc. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
describe("Test262 Promise static adaptations", () => {
test("Promise statics are callable", async () => {
expect(
await value(
`return [typeof Promise.all, typeof Promise.allSettled, typeof Promise.race, typeof Promise.resolve, typeof Promise.reject]`,
),
).toEqual(["function", "function", "function", "function", "function"])
})
test("invalid combinator inputs return rejected TypeError promises", async () => {
expect(
await value(`
const observe = async (run) => {
let returned = false
try {
const promise = run()
returned = promise instanceof Promise
await promise
return [returned, "fulfilled"]
} catch (error) {
return [returned, error.name]
}
}
return await Promise.all([
observe(() => Promise.all(42)),
observe(() => Promise.allSettled(42)),
observe(() => Promise.race(42)),
])
`),
).toEqual([
[true, "TypeError"],
[true, "TypeError"],
[true, "TypeError"],
])
})
test("Promise.all returns a promise and creates a fresh empty array", async () => {
expect(
await value(`
const input = []
const promise = Promise.all(input)
const result = await promise
return [promise instanceof Promise, result instanceof Array, result.length, result !== input]
`),
).toEqual([true, true, 0, true])
})
test("Promise.all preserves values and input order", async () => {
expect(
await value(`
const first = { id: 1 }
const second = { id: 2 }
const result = await Promise.all([Promise.resolve(3), first, Promise.resolve(second)])
return [result.length, result[0], result[1] === first, result[2] === second]
`),
).toEqual([3, 3, true, true])
})
test("Promise.all adopts rejection from either input position", async () => {
expect(
await value(`
const observe = async (promise) => {
try { await promise; return "fulfilled" } catch (reason) { return reason }
}
return await Promise.all([
observe(Promise.all([Promise.reject(1), Promise.resolve(2)])),
observe(Promise.all([Promise.resolve(1), Promise.reject(2)])),
])
`),
).toEqual([1, 2])
})
test("Promise.all treats sparse positions as undefined values", async () => {
expect(
await value(`
const input = []
input[1] = 1
const result = await Promise.all(input)
return [result.length, result[0] === undefined, result[1]]
`),
).toEqual([2, true, 1])
})
test("Promise.allSettled returns a promise and a fresh array", async () => {
expect(
await value(`
const input = []
const promise = Promise.allSettled(input)
const result = await promise
return [promise instanceof Promise, result instanceof Array, result.length, result !== input]
`),
).toEqual([true, true, 0, true])
})
test("Promise.allSettled preserves order and settlement shapes", async () => {
expect(
await value(`
const reason = { id: 4 }
const result = await Promise.allSettled([
Promise.resolve(1),
Promise.reject(2),
3,
Promise.reject(reason),
])
return [result, result.map((item) => Object.keys(item))]
`),
).toEqual([
[
{ status: "fulfilled", value: 1 },
{ status: "rejected", reason: 2 },
{ status: "fulfilled", value: 3 },
{ status: "rejected", reason: { id: 4 } },
],
[
["status", "value"],
["status", "reason"],
["status", "value"],
["status", "reason"],
],
])
})
test("Promise.allSettled preserves input order across settlement timing", async () => {
expect(
await value(`
const slow = async () => { await Promise.resolve(); return "slow" }
return await Promise.allSettled([slow(), Promise.resolve("fast")])
`),
).toEqual([
{ status: "fulfilled", value: "slow" },
{ status: "fulfilled", value: "fast" },
])
})
test("Promise.allSettled adopts a non-thenable object", async () => {
expect(
await value(`
const object = { id: 1 }
const result = await Promise.allSettled([object])
return [result, result[0].value === object]
`),
).toEqual([[{ status: "fulfilled", value: { id: 1 } }], true])
})
test("Promise.allSettled treats sparse positions as undefined values", async () => {
expect(
await value(`
const input = []
input[1] = 1
const result = await Promise.allSettled(input)
return [result.length, result[0].status, result[0].value === undefined, result[1]]
`),
).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
})
test("Promise.race returns a promise and settles from its first input", async () => {
expect(
await value(`
const rejected = Promise.reject(2)
const fulfilled = Promise.resolve(1)
const promise = Promise.race([fulfilled, rejected])
return [promise instanceof Promise, await promise]
`),
).toEqual([true, 1])
})
test("Promise.race preserves primitive fulfillment and rejection", async () => {
expect(
await value(`
const fulfilled = await Promise.race([23])
let rejected
try { await Promise.race([Promise.reject(7)]) } catch (reason) { rejected = reason }
return [fulfilled, rejected]
`),
).toEqual([23, 7])
})
test("Promise.race observes later fulfillment and rejection", async () => {
expect(
await value(`
const slow = async () => { await Promise.resolve(); await Promise.resolve(); return 1 }
const rejectSoon = async () => { await Promise.resolve(); throw 2 }
try { await Promise.race([slow(), rejectSoon()]); return "fulfilled" } catch (reason) { return reason }
`),
).toBe(2)
})
test("Promise.race selects fulfilled and rejected inputs by settlement order", async () => {
expect(
await value(`
const delayed = async (value) => { await Promise.resolve(); return value }
const first = await Promise.race([Promise.resolve(1), Promise.resolve(2)])
const second = await Promise.race([Promise.resolve(1), delayed(9)])
const third = await Promise.race([delayed(9), Promise.resolve(2)])
let fourth
try { await Promise.race([Promise.reject(1), Promise.resolve(2)]) } catch (reason) { fourth = reason }
return [first, second, third, fourth]
`),
).toEqual([1, 1, 2, 1])
})
test("Promise.race treats a sparse first position as undefined", async () => {
expect(
await value(`
const input = []
input[1] = 1
return (await Promise.race(input)) === undefined
`),
).toBe(true)
})
test("Promise.resolve adopts values and nested promises", async () => {
expect(
await value(`
const object = { id: 1 }
return [
await Promise.resolve(42),
await Promise.resolve(Promise.resolve("nested")),
(await Promise.resolve(object)) === object,
]
`),
).toEqual([42, "nested", true])
})
test("Promise.resolve preserves promise identity", async () => {
expect(
await value(`
const promise = Promise.resolve(1)
const identities = new Map([[promise, "same"]])
return identities.get(Promise.resolve(promise))
`),
).toBe("same")
})
test("Promise.reject preserves primitive and object reasons", async () => {
expect(
await value(`
const object = { reason: true }
const reasons = [undefined, null, false, true, 0, "", 42, object]
const observe = async (reason) => {
try { await Promise.reject(reason); return false } catch (caught) { return caught === reason }
}
return await Promise.all(reasons.map(observe))
`),
).toEqual([true, true, true, true, true, true, true, true])
})
})
describe("Test262 async function adaptations", () => {
test("async functions return promises and adopt returned values", async () => {
expect(
await value(`
const plain = async () => 42
const afterAwait = async () => { await Promise.resolve(); return 43 }
const first = plain()
const second = afterAwait()
return [first instanceof Promise, second instanceof Promise, await first, await second]
`),
).toEqual([true, true, 42, 43])
})
test("async functions reject for throws before and after await", async () => {
expect(
await value(`
const throwsBefore = async () => { throw 1 }
const throwsAfter = async () => { await Promise.resolve(); throw 2 }
const observe = async (promise) => {
try { await promise; return "fulfilled" } catch (reason) { return reason }
}
return await Promise.all([observe(throwsBefore()), observe(throwsAfter())])
`),
).toEqual([1, 2])
})
})
+314 -68
View File
@@ -92,33 +92,14 @@ describe("first-class promise values", () => {
}
const first = load(1)
const second = load(2)
return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])]
return await Promise.all([first, second])
`),
).toEqual([
true,
true,
[
[1, 1],
[2, 2],
],
[1, 1],
[2, 2],
])
})
test("async function errors reject instead of throwing at the call site", async () => {
expect(
await value(`
const fail = async () => { throw new Error("boom") }
const promise = fail()
try {
await promise
return "no"
} catch (error) {
return error.message
}
`),
).toBe("boom")
})
test("an un-awaited tool call starts eagerly, in call order, before any await", async () => {
const trace = makeTrace()
const result = await value(
@@ -212,6 +193,22 @@ describe("first-class promise values", () => {
expect(diagnostic.suggestions?.join(" ")).toContain("Await promises")
})
test("an unhandled rejection does not stop later work from draining", async () => {
const trace = makeTrace()
const diagnostic = await error(
`
tools.host.fail({})
tools.host.sleepy({ id: 1, ms: 30 })
return "done"
`,
{ trace },
)
expect(diagnostic.kind).toBe("ToolFailure")
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
test("a never-awaited failing async function surfaces as an unhandled promise rejection", async () => {
const diagnostic = await error(`
const fail = async () => { throw new Error("boom") }
@@ -235,6 +232,58 @@ describe("first-class promise values", () => {
expect(diagnostic.kind).toBe("ToolFailure")
expect(diagnostic.message).toContain("Lookup refused")
})
test("keeps unreturned calls alive after their async function settles", async () => {
const trace = makeTrace()
expect(
await value(
`
const start = async () => {
tools.host.sleepy({ id: 1, ms: 30 })
return "started"
}
await start()
return "done"
`,
{ trace },
),
).toBe("done")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
test("keeps unreturned calls alive after their promise handler settles", async () => {
const trace = makeTrace()
expect(
await value(
`
await Promise.resolve().then(() => {
tools.host.sleepy({ id: 1, ms: 30 })
})
return "done"
`,
{ trace },
),
).toBe("done")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
test("drains pending work after program failure and preserves the original failure", async () => {
const trace = makeTrace()
const diagnostic = await error(
`
tools.host.fail({})
tools.host.sleepy({ id: 1, ms: 30 })
throw new Error("original")
`,
{ trace },
)
expect(diagnostic.kind).toBe("ExecutionFailure")
expect(diagnostic.message).toBe("Uncaught: original")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
})
describe("promises at data boundaries", () => {
@@ -245,6 +294,12 @@ describe("promises at data boundaries", () => {
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
})
test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("un-awaited Promise")
})
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
expect(diagnostic.kind).toBe("InvalidDataValue")
@@ -329,10 +384,6 @@ describe("Promise.all over arbitrary arrays", () => {
expect(trace.maxActive).toBeLessThanOrEqual(8)
})
test("resolves the empty array", async () => {
expect(await value(`return await Promise.all([])`)).toEqual([])
})
test("rejects with the first failure, catchable in-program", async () => {
expect(
await value(`
@@ -412,45 +463,36 @@ describe("Promise.allSettled", () => {
})
describe("Promise.race", () => {
test("first settlement wins and losers are interrupted", async () => {
test("first settlement wins and losers continue", async () => {
const trace = makeTrace()
const result = await value(
`
const fast = tools.host.sleepy({ id: 1, ms: 10 })
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
const slow = tools.host.sleepy({ id: 2, ms: 30 })
return await Promise.race([fast, slow])
`,
{ trace },
)
expect(result).toBe(1)
expect(trace.interrupted).toBe(1)
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
expect(trace.completed).toBe(2)
})
test("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
test("a losing chain continues and remains awaitable", async () => {
expect(
await value(`
const fast = tools.host.sleepy({ id: 1, ms: 10 })
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
const winner = await Promise.race([fast, slow])
try {
await slow
return "no"
} catch (e) {
return { winner, caught: e.message }
}
const slow = tools.host.sleepy({ id: 2, ms: 30 }).then((id) => id * 2)
const winner = await Promise.race([slow, "fast"])
return [winner, await slow]
`),
).toEqual({
winner: 1,
caught: "This tool call was interrupted because another value settled a Promise.race first.",
})
).toEqual(["fast", 4])
})
test("a rejection can win the race", async () => {
expect(
await value(`
try {
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })])
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 30 })])
return "no"
} catch (e) {
return e.message
@@ -459,12 +501,37 @@ describe("Promise.race", () => {
).toBe("Lookup refused")
})
test("a rejecting winner still drains its slow loser", async () => {
const trace = makeTrace()
const diagnostic = await error(
`return await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 30 })])`,
{ trace },
)
expect(diagnostic.kind).toBe("ToolFailure")
expect(diagnostic.message).toBe("Lookup refused")
expect(trace.completed).toBe(1)
expect(trace.interrupted).toBe(0)
})
test("a plain value wins over pending promises", async () => {
const trace = makeTrace()
expect(
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }),
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 30 }), "immediate"])`, { trace }),
).toBe("immediate")
expect(trace.interrupted).toBe(1)
expect(trace.interrupted).toBe(0)
expect(trace.completed).toBe(1)
})
test("a losing rejection remains handled", async () => {
expect(
await value(`
const rejectLater = async () => {
await tools.host.sleepy({ id: 1, ms: 20 })
throw new Error("late")
}
return await Promise.race([rejectLater(), "winner"])
`),
).toBe("winner")
})
test("an empty race is a clear error instead of hanging", async () => {
@@ -474,23 +541,35 @@ describe("Promise.race", () => {
})
describe("Promise.resolve / Promise.reject", () => {
test("resolve wraps plain values and passes promises through", async () => {
expect(await value(`return await Promise.resolve(42)`)).toBe(42)
expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested")
test("resolve adopts a tool-call promise", async () => {
expect(await value(`return await Promise.resolve(tools.host.sleepy({ id: 3 }))`)).toBe(3)
})
test("reject produces a promise whose await throws the reason", async () => {
test("an abandoned rejected promise surfaces as an unhandled rejection", async () => {
const diagnostic = await error(`
Promise.reject(new Error("boom"))
return "done"
`)
expect(diagnostic.kind).toBe("ExecutionFailure")
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
expect(diagnostic.message).toContain("boom")
})
})
describe("Promise combinator values", () => {
test("all, allSettled, and race return chainable promises", async () => {
expect(
await value(`
try {
await Promise.reject("nope")
return "no"
} catch (e) {
return e
}
`),
).toBe("nope")
const all = Promise.all([Promise.resolve(1), 2])
const settled = Promise.allSettled([Promise.resolve(3)])
const race = Promise.race([Promise.resolve(4)])
return [
await all.then((values) => values.join(",")),
await settled.then((values) => values[0].value),
await race.then((value) => value + 1),
]
`),
).toEqual(["1,2", 3, 5])
})
})
@@ -525,18 +604,185 @@ describe("timeout interruption of forked calls", () => {
expect(result.error.kind).toBe("TimeoutExceeded")
expect(trace.interrupted).toBe(2)
})
test("interrupts promise fibers concurrently during scope teardown", async () => {
const cleanup = { active: 0, overlapped: false }
const tool = Tool.make({
description: "Wait until interrupted",
input: Schema.Struct({}),
output: Schema.Never,
run: () =>
Effect.never.pipe(
Effect.onInterrupt(() =>
Effect.gen(function* () {
cleanup.active += 1
yield* Effect.sleep(20)
cleanup.overlapped ||= cleanup.active > 1
cleanup.active -= 1
}),
),
),
})
const result = await Effect.runPromise(
CodeMode.execute({
tools: { host: { first: tool, second: tool } },
code: `
tools.host.first({})
tools.host.second({})
return await Promise.resolve("waiting")
`,
limits: { timeoutMs: 50 },
}),
)
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.error.kind).toBe("TimeoutExceeded")
expect(cleanup.overlapped).toBe(true)
})
})
describe("promise chaining", () => {
test("then transforms values and flattens returned promises", async () => {
expect(
await value(`
return tools.host.sleepy({ id: 2 })
.then((id) => tools.host.sleepy({ id: id + 1 }))
.then((id) => id * 2)
`),
).toBe(6)
})
test("handlers run after synchronous statements", async () => {
expect(
await value(`
const order = []
const chained = Promise.resolve().then(() => order.push("then"))
order.push("sync")
await chained
return order
`),
).toEqual(["sync", "then"])
})
test("nested reactions run before downstream reactions queued later", async () => {
expect(
await value(`
const order = []
await Promise.resolve()
.then(() => {
order.push(1)
Promise.resolve().then(() => order.push(2))
})
.then(() => order.push(3))
return order
`),
).toEqual([1, 2, 3])
})
test("plain and settled await resume after reactions that are already queued", async () => {
expect(
await value(`
const order = []
Promise.resolve().then(() => order.push(1))
await 0
order.push(2)
Promise.resolve().then(() => order.push(3))
await Promise.resolve()
order.push(4)
return order
`),
).toEqual([1, 2, 3, 4])
})
test("reactions registered on the same pending promise preserve order", async () => {
expect(
await value(`
const order = []
const pending = tools.host.sleepy({ id: 1 })
pending.then(() => order.push(1))
pending.then(() => order.push(2))
await pending
return order
`),
).toEqual([1, 2])
})
test("an async reaction does not block the next queued reaction", async () => {
expect(
await value(`
const order = []
const first = Promise.resolve().then(async () => {
order.push(1)
await tools.host.sleepy({ id: 1 })
order.push(3)
})
Promise.resolve().then(() => order.push(2))
await first
return order
`),
).toEqual([1, 2, 3])
})
test("catch receives normalized errors and recovers the chain", async () => {
expect(await value(`return tools.host.fail({}).catch((error) => error.message)`)).toBe("Lookup refused")
expect(
await value(`
return Promise.resolve(1)
.then(() => { throw new Error("boom") })
.catch((error) => error.message)
`),
).toBe("boom")
})
test("then rejection handlers and omitted handlers pass through settlement", async () => {
expect(await value(`return Promise.resolve(4).then(undefined).catch(undefined)`)).toBe(4)
expect(await value(`return Promise.reject("nope").then(undefined, (reason) => reason + "!")`)).toBe("nope!")
})
test("supported builtin callables can be handlers", async () => {
expect(await value(`return Promise.resolve({ a: 1 }).then(JSON.stringify)`)).toBe('{"a":1}')
expect(await value(`return Promise.resolve(4).then(Promise.resolve)`)).toBe(4)
})
test("finally awaits its callback and preserves the original settlement", async () => {
expect(
await value(`
let cleanup = 0
const result = await Promise.resolve(7).finally(async () => {
await tools.host.sleepy({ id: 1 })
cleanup = 1
return 99
})
return [result, cleanup]
`),
).toEqual([7, 1])
expect(
await value(`return Promise.reject(new Error("original")).finally(() => 99).catch((error) => error.message)`),
).toBe("original")
})
test("a rejected finally callback replaces the original settlement", async () => {
expect(
await value(`
return Promise.resolve(1)
.finally(() => Promise.reject(new Error("cleanup")))
.catch((error) => error.message)
`),
).toBe("cleanup")
})
test("a self-resolving chain rejects instead of deadlocking", async () => {
expect(
await value(`
let chained
chained = Promise.resolve().then(() => chained)
return chained.catch((error) => [error.name, error.message])
`),
).toEqual(["TypeError", "Chaining cycle detected for promise."])
})
})
describe("unsupported promise surface", () => {
test(".then/.catch/.finally give a clear await-instead error", async () => {
for (const method of ["then", "catch", "finally"]) {
const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).${method}((x) => x)`)
expect(diagnostic.kind).toBe("UnsupportedSyntax")
expect(diagnostic.message).toContain(`Promise.prototype.${method} is not supported`)
expect(diagnostic.message).toContain("await")
}
})
test("other property reads on a promise hint at the missing await", async () => {
const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).value`)
expect(diagnostic.kind).toBe("InvalidDataValue")
+108 -19
View File
@@ -19,6 +19,28 @@ const error = async (code: string) => {
return result.error
}
describe("Number and Math", () => {
test("Math.random returns a number in [0, 1)", async () => {
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
})
test("Number exposes native non-finite constants", async () => {
expect(
await value(
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
),
).toEqual([true, true, true])
})
test("Number valueOf returns its primitive receiver", async () => {
expect(await value(`return (42).valueOf()`)).toBe(42)
})
test("Number valueOf does not enable boxed numbers", async () => {
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
})
})
describe("Date", () => {
test("Date.now() returns a number", async () => {
expect(await value(`return typeof Date.now()`)).toBe("number")
@@ -132,9 +154,7 @@ describe("RegExp", () => {
).toEqual(["1", "22"])
})
test("string match: non-global carries index, global lists all matches", async () => {
expect(await value(`const m = "a1b22".match(/\\d+/); return [m[0], m.index]`)).toEqual(["1", 1])
expect(await value(`return "a1b22".match(/\\d+/g)`)).toEqual(["1", "22"])
test("an unmatched string pattern returns null", async () => {
expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
})
@@ -142,13 +162,6 @@ describe("RegExp", () => {
expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
})
test("replace and replaceAll with patterns and $1 substitution", async () => {
expect(await value(`return "a1b2".replace(/\\d/, "#")`)).toBe("a#b2")
expect(await value(`return "a1b2".replace(/\\d/g, "#")`)).toBe("a#b#")
expect(await value(`return "a1b2".replaceAll(/\\d/g, "#")`)).toBe("a#b#")
expect(await value(`return "hi bob".replace(/b(o)b/, "[$1]")`)).toBe("hi [o]")
})
test("function replacers receive captures, offsets, input, and named groups", async () => {
expect(
await value(`
@@ -214,12 +227,6 @@ describe("RegExp", () => {
expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
})
test("split and search accept patterns", async () => {
expect(await value(`return "a1b22c".split(/\\d+/)`)).toEqual(["a", "b", "c"])
expect(await value(`return "ab42".search(/\\d/)`)).toBe(2)
expect(await value(`return "ab".search(/\\d/)`)).toBe(-1)
})
test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
@@ -628,9 +635,7 @@ describe("stdlib integration", () => {
true,
)
expect(
await value(
`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`,
),
await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`),
).toBe(true)
})
@@ -751,6 +756,43 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
)
})
test("Object.values/entries preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.values(rows)[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.entries(rows)[0][1].selected = true
return child.selected
`),
).toBe(true)
})
test("Object enumeration preserves promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
const source = { pending }
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
`),
).toEqual([["pending"], true, 1, 1])
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
})
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
})
test("Object.assign keeps Maps usable", async () => {
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
1,
@@ -773,6 +815,53 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
})
test("Array.from and Array.of preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
Array.from([child])[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
Array.of(child)[0].selected = true
return child.selected
`),
).toBe(true)
})
test("Array.from and Array.of preserve promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
return [await Array.from([pending])[0], await Array.of(pending)[0]]
`),
).toEqual([1, 1])
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
})
test("Array.from preserves identity across supported collection shapes", async () => {
expect(
await value(`
const child = { selected: false }
const fromArrayLike = Array.from({ 0: child, length: 1 })
const fromMap = Array.from(new Map([["child", child]]))
const fromSet = Array.from(new Set([child]))
fromArrayLike[0].selected = true
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
`),
).toEqual([true, true, true])
})
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
})
test("regexes stay callable through Object.values", async () => {
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
})
@@ -0,0 +1,580 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js
* - test/built-ins/String/prototype/toLowerCase/special_casing.js
* - test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js
* - test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js
* - test/built-ins/String/prototype/toLowerCase/supplementary_plane.js
* - test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js
* - test/built-ins/String/prototype/toUpperCase/special_casing.js
* - test/built-ins/String/prototype/toUpperCase/supplementary_plane.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-1.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-2.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-3.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-4.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-5.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-6.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-7.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-8.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-9.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-10.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-11.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-12.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-13.js
* - test/built-ins/String/prototype/trim/15.5.4.20-3-14.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-1.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-2.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-3.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-4.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-5.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-6.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-8.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-10.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-11.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-12.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-13.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-14.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-16.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-18.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-19.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-20.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-21.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-22.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-24.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-27.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-28.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-29.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-30.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-32.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-34.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-35.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-36.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-37.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-38.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-39.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-40.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-41.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-42.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-43.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-44.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-45.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-46.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-47.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-48.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-49.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-50.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-51.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-52.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-53.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-54.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-55.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-56.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-57.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-58.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-59.js
* - test/built-ins/String/prototype/trim/15.5.4.20-4-60.js
* - test/built-ins/String/prototype/trim/u180e.js
* - test/built-ins/String/prototype/trimStart/this-value-whitespace.js
* - test/built-ins/String/prototype/trimStart/this-value-line-terminator.js
* - test/built-ins/String/prototype/trimEnd/this-value-whitespace.js
* - test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js
* - test/built-ins/String/prototype/repeat/repeat-string-n-times.js
* - test/built-ins/String/prototype/repeat/empty-string-returns-empty.js
* - test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js
* - test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js
* - test/built-ins/String/prototype/padStart/fill-string-empty.js
* - test/built-ins/String/prototype/padStart/normal-operation.js
* - test/built-ins/String/prototype/padStart/fill-string-omitted.js
* - test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js
* - test/built-ins/String/prototype/padEnd/fill-string-empty.js
* - test/built-ins/String/prototype/padEnd/normal-operation.js
* - test/built-ins/String/prototype/padEnd/fill-string-omitted.js
* - test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js
* - test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js
* - test/built-ins/String/prototype/charAt/S9.4_A1.js
* - test/built-ins/String/prototype/charAt/S9.4_A2.js
* - test/built-ins/String/prototype/charAt/pos-rounding.js
* - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js
* - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js
* - test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js
* - test/built-ins/String/prototype/charCodeAt/pos-rounding.js
* - test/built-ins/String/prototype/codePointAt/return-single-code-unit.js
* - test/built-ins/String/prototype/codePointAt/return-first-code-unit.js
* - test/built-ins/String/prototype/codePointAt/return-utf16-decode.js
* - test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js
* - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js
* - test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js
* - test/built-ins/String/prototype/at/returns-code-unit.js
* - test/built-ins/String/prototype/at/returns-item.js
* - test/built-ins/String/prototype/at/returns-item-relative-index.js
* - test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js
* - test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js
* - test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js
* - test/built-ins/String/prototype/toString/string-primitive.js
* - test/built-ins/String/prototype/normalize/return-normalized-string.js
* - test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js
* - test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js
* - test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js
* - test/built-ins/String/fromCharCode/S15.5.3.2_A2.js
* - test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js
* - test/built-ins/String/fromCharCode/S9.7_A1.js
* - test/built-ins/String/fromCharCode/S9.7_A2.1.js
* - test/built-ins/String/fromCharCode/S9.7_A2.2.js
* - test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js
* - test/built-ins/String/fromCodePoint/arguments-is-empty.js
* - test/built-ins/String/fromCodePoint/return-string-value.js
* - test/built-ins/String/fromCodePoint/argument-is-not-integer.js
* - test/built-ins/String/fromCodePoint/number-is-out-of-range.js
*
* Copyright 2009 the Sputnik authors. All rights reserved.
* Copyright (C) 2009 the Sputnik authors. All rights reserved.
* Copyright (c) 2012 Ecma International. All rights reserved.
* Copyright 2012 Norbert Lindenberg. All rights reserved.
* Copyright 2012 Mozilla Corporation. All rights reserved.
* Copyright 2013 Microsoft Corporation. All rights reserved.
* Copyright (C) 2015 the V8 project authors. All rights reserved.
* Copyright (C) 2015 André Bargull. All rights reserved.
* Copyright (C) 2016 the V8 project authors. All rights reserved.
* Copyright (C) 2016 André Bargull. All rights reserved.
* Copyright (C) 2016 Jordan Harband. All rights reserved.
* Copyright (C) 2016 Mathias Bynens. All rights reserved.
* Copyright (c) 2017 Valerie Young. All rights reserved.
* Copyright (C) 2017 Valerie Young. All rights reserved.
* Copyright (C) 2020 Rick Waldron. All rights reserved.
* Copyright (C) 2022 Richard Gibson. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
type Argument = string | number | undefined
type Outcome = "undefined" | "length" | "RangeError"
type Assertion = {
label: string
input?: string
args?: ReadonlyArray<Argument>
expected?: string | number
outcome?: Outcome
}
type Vector = {
path: string
method: string
static?: boolean
assertions: ReadonlyArray<Assertion>
}
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const literal = (input: Argument) => {
if (input === undefined) return "undefined"
if (typeof input === "string") return JSON.stringify(input)
if (Number.isNaN(input)) return "NaN"
if (input === Infinity) return "Infinity"
if (input === -Infinity) return "-Infinity"
if (Object.is(input, -0)) return "-0"
return JSON.stringify(input)
}
const vectors: Array<Vector> = []
const add = (path: string, method: string, assertions: ReadonlyArray<Assertion>, staticMethod = false) => {
vectors.push({ path, method, assertions, static: staticMethod })
}
const assertion = (label: string, input: string, expected: string | number, args: ReadonlyArray<Argument> = []) => ({
label,
input,
args,
expected,
})
add("test/built-ins/String/prototype/toLowerCase/S15.5.4.16_A2_T1.js", "toLowerCase", [
assertion("#1 direct value", "Hello, WoRlD!", "hello, world!"),
assertion("#2 String value", "Hello, WoRlD!", "hello, world!"),
])
add("test/built-ins/String/prototype/toLowerCase/special_casing.js", "toLowerCase", [
assertion(
"103 SpecialCasing mappings",
"\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7",
"\u00DF\u0069\u0307\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FB3\u1FB3\u1FC3\u1FC3\u1FF3\u1FF3\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7",
),
])
add("test/built-ins/String/prototype/toLowerCase/special_casing_conditional.js", "toLowerCase", [
assertion("single sigma", "\u03A3", "\u03C3"),
assertion("preceded by cased", "A\u03A3", "a\u03C2"),
assertion("preceded by supplementary cased", "\uD835\uDCA2\u03A3", "\uD835\uDCA2\u03C2"),
assertion("preceded by full stop", "A.\u03A3", "a.\u03C2"),
assertion("preceded by soft hyphen", "A\u00AD\u03A3", "a\u00AD\u03C2"),
assertion("preceded by combining mark", "A\uD834\uDE42\u03A3", "a\uD834\uDE42\u03C2"),
assertion("preceded by uncased combining mark", "\u0345\u03A3", "\u0345\u03C3"),
assertion("preceded by cased and combining mark", "\u0391\u0345\u03A3", "\u03B1\u0345\u03C2"),
assertion("followed by cased", "A\u03A3B", "a\u03C3b"),
assertion("followed by supplementary cased", "A\u03A3\uD835\uDCA2", "a\u03C3\uD835\uDCA2"),
assertion("followed by full stop and cased", "A\u03A3.b", "a\u03C3.b"),
assertion("followed by soft hyphen and cased", "A\u03A3\u00ADB", "a\u03C3\u00ADb"),
assertion("followed by combining mark and cased", "A\u03A3\uD834\uDE42B", "a\u03C3\uD834\uDE42b"),
assertion("followed by uncased combining mark", "A\u03A3\u0345", "a\u03C2\u0345"),
assertion("followed by combining mark and cased Greek", "A\u03A3\u0345\u0391", "a\u03C3\u0345\u03B1"),
])
add("test/built-ins/String/prototype/toLowerCase/Final_Sigma_U180E.js", "toLowerCase", [
assertion("preceded by U+180E", "A\u180E\u03A3", "a\u180E\u03C2"),
assertion("preceded by U+180E and followed by cased", "A\u180E\u03A3B", "a\u180E\u03C3b"),
assertion("followed by U+180E", "A\u03A3\u180E", "a\u03C2\u180E"),
assertion("followed by U+180E and cased", "A\u03A3\u180EB", "a\u03C3\u180Eb"),
assertion("surrounded by U+180E", "A\u180E\u03A3\u180E", "a\u180E\u03C2\u180E"),
assertion("surrounded by U+180E and followed by cased", "A\u180E\u03A3\u180EB", "a\u180E\u03C3\u180Eb"),
])
add("test/built-ins/String/prototype/toLowerCase/supplementary_plane.js", "toLowerCase", [
assertion(
"40 Deseret mappings",
"\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27",
"\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F",
),
])
add("test/built-ins/String/prototype/toUpperCase/S15.5.4.18_A2_T1.js", "toUpperCase", [
assertion("#1 direct value", "Hello, WoRlD!", "HELLO, WORLD!"),
assertion("#2 String value", "Hello, WoRlD!", "HELLO, WORLD!"),
])
add("test/built-ins/String/prototype/toUpperCase/special_casing.js", "toUpperCase", [
assertion(
"103 SpecialCasing mappings",
"\u00DF\u0130\uFB00\uFB01\uFB02\uFB03\uFB04\uFB05\uFB06\u0587\uFB13\uFB14\uFB15\uFB16\uFB17\u0149\u0390\u03B0\u01F0\u1E96\u1E97\u1E98\u1E99\u1E9A\u1F50\u1F52\u1F54\u1F56\u1FB6\u1FC6\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2\u1FE3\u1FE4\u1FE6\u1FE7\u1FF6\u1F80\u1F81\u1F82\u1F83\u1F84\u1F85\u1F86\u1F87\u1F88\u1F89\u1F8A\u1F8B\u1F8C\u1F8D\u1F8E\u1F8F\u1F90\u1F91\u1F92\u1F93\u1F94\u1F95\u1F96\u1F97\u1F98\u1F99\u1F9A\u1F9B\u1F9C\u1F9D\u1F9E\u1F9F\u1FA0\u1FA1\u1FA2\u1FA3\u1FA4\u1FA5\u1FA6\u1FA7\u1FA8\u1FA9\u1FAA\u1FAB\u1FAC\u1FAD\u1FAE\u1FAF\u1FB3\u1FBC\u1FC3\u1FCC\u1FF3\u1FFC\u1FB2\u1FB4\u1FC2\u1FC4\u1FF2\u1FF4\u1FB7\u1FC7\u1FF7",
"\u0053\u0053\u0130\u0046\u0046\u0046\u0049\u0046\u004C\u0046\u0046\u0049\u0046\u0046\u004C\u0053\u0054\u0053\u0054\u0535\u0552\u0544\u0546\u0544\u0535\u0544\u053B\u054E\u0546\u0544\u053D\u02BC\u004E\u0399\u0308\u0301\u03A5\u0308\u0301\u004A\u030C\u0048\u0331\u0054\u0308\u0057\u030A\u0059\u030A\u0041\u02BE\u03A5\u0313\u03A5\u0313\u0300\u03A5\u0313\u0301\u03A5\u0313\u0342\u0391\u0342\u0397\u0342\u0399\u0308\u0300\u0399\u0308\u0301\u0399\u0342\u0399\u0308\u0342\u03A5\u0308\u0300\u03A5\u0308\u0301\u03A1\u0313\u03A5\u0342\u03A5\u0308\u0342\u03A9\u0342\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F08\u0399\u1F09\u0399\u1F0A\u0399\u1F0B\u0399\u1F0C\u0399\u1F0D\u0399\u1F0E\u0399\u1F0F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F28\u0399\u1F29\u0399\u1F2A\u0399\u1F2B\u0399\u1F2C\u0399\u1F2D\u0399\u1F2E\u0399\u1F2F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u1F68\u0399\u1F69\u0399\u1F6A\u0399\u1F6B\u0399\u1F6C\u0399\u1F6D\u0399\u1F6E\u0399\u1F6F\u0399\u0391\u0399\u0391\u0399\u0397\u0399\u0397\u0399\u03A9\u0399\u03A9\u0399\u1FBA\u0399\u0386\u0399\u1FCA\u0399\u0389\u0399\u1FFA\u0399\u038F\u0399\u0391\u0342\u0399\u0397\u0342\u0399\u03A9\u0342\u0399",
),
])
add("test/built-ins/String/prototype/toUpperCase/supplementary_plane.js", "toUpperCase", [
assertion(
"40 Deseret mappings",
"\uD801\uDC28\uD801\uDC29\uD801\uDC2A\uD801\uDC2B\uD801\uDC2C\uD801\uDC2D\uD801\uDC2E\uD801\uDC2F\uD801\uDC30\uD801\uDC31\uD801\uDC32\uD801\uDC33\uD801\uDC34\uD801\uDC35\uD801\uDC36\uD801\uDC37\uD801\uDC38\uD801\uDC39\uD801\uDC3A\uD801\uDC3B\uD801\uDC3C\uD801\uDC3D\uD801\uDC3E\uD801\uDC3F\uD801\uDC40\uD801\uDC41\uD801\uDC42\uD801\uDC43\uD801\uDC44\uD801\uDC45\uD801\uDC46\uD801\uDC47\uD801\uDC48\uD801\uDC49\uD801\uDC4A\uD801\uDC4B\uD801\uDC4C\uD801\uDC4D\uD801\uDC4E\uD801\uDC4F",
"\uD801\uDC00\uD801\uDC01\uD801\uDC02\uD801\uDC03\uD801\uDC04\uD801\uDC05\uD801\uDC06\uD801\uDC07\uD801\uDC08\uD801\uDC09\uD801\uDC0A\uD801\uDC0B\uD801\uDC0C\uD801\uDC0D\uD801\uDC0E\uD801\uDC0F\uD801\uDC10\uD801\uDC11\uD801\uDC12\uD801\uDC13\uD801\uDC14\uD801\uDC15\uD801\uDC16\uD801\uDC17\uD801\uDC18\uD801\uDC19\uD801\uDC1A\uD801\uDC1B\uD801\uDC1C\uD801\uDC1D\uD801\uDC1E\uD801\uDC1F\uD801\uDC20\uD801\uDC21\uD801\uDC22\uD801\uDC23\uD801\uDC24\uD801\uDC25\uD801\uDC26\uD801\uDC27",
),
])
const whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"
const lineTerminators = "\u000A\u000D\u2028\u2029"
const trim = (file: string, input: string, expected: string) =>
add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)])
trim("15.5.4.20-3-1.js", lineTerminators, "")
trim("15.5.4.20-3-2.js", whitespace, "")
trim("15.5.4.20-3-3.js", whitespace + lineTerminators, "")
trim("15.5.4.20-3-4.js", whitespace + lineTerminators + "abc", "abc")
trim("15.5.4.20-3-5.js", "abc" + whitespace + lineTerminators, "abc")
trim("15.5.4.20-3-6.js", whitespace + lineTerminators + "abc" + whitespace + lineTerminators, "abc")
trim("15.5.4.20-3-7.js", "ab" + whitespace + lineTerminators + "cd", "ab" + whitespace + lineTerminators + "cd")
trim("15.5.4.20-3-8.js", "\0\u0000", "\0\u0000")
trim("15.5.4.20-3-9.js", "\0", "\0")
trim("15.5.4.20-3-10.js", "\u0000", "\u0000")
trim("15.5.4.20-3-11.js", "\0\u0000abc", "\0\u0000abc")
trim("15.5.4.20-3-12.js", "abc\0\u0000", "abc\0\u0000")
trim("15.5.4.20-3-13.js", "\0\u0000abc\0\u0000", "\0\u0000abc\0\u0000")
trim("15.5.4.20-3-14.js", "a\0\u0000bc", "a\0\u0000bc")
trim("15.5.4.20-4-1.js", "\u0009a bc \u0009", "a bc")
trim("15.5.4.20-4-2.js", " \u0009abc \u0009", "abc")
trim("15.5.4.20-4-3.js", "\u0009abc", "abc")
trim("15.5.4.20-4-4.js", "\u000Babc", "abc")
trim("15.5.4.20-4-5.js", "\u000Cabc", "abc")
trim("15.5.4.20-4-6.js", "\u0020abc", "abc")
trim("15.5.4.20-4-8.js", "\u00A0abc", "abc")
trim("15.5.4.20-4-10.js", "\uFEFFabc", "abc")
trim("15.5.4.20-4-11.js", "abc\u0009", "abc")
trim("15.5.4.20-4-12.js", "abc\u000B", "abc")
trim("15.5.4.20-4-13.js", "abc\u000C", "abc")
trim("15.5.4.20-4-14.js", "abc\u0020", "abc")
trim("15.5.4.20-4-16.js", "abc\u00A0", "abc")
trim("15.5.4.20-4-18.js", "abc\uFEFF", "abc")
trim("15.5.4.20-4-19.js", "\u0009abc\u0009", "abc")
trim("15.5.4.20-4-20.js", "\u000Babc\u000B", "abc")
trim("15.5.4.20-4-21.js", "\u000Cabc\u000C", "abc")
trim("15.5.4.20-4-22.js", "\u0020abc\u0020", "abc")
trim("15.5.4.20-4-24.js", "\u00A0abc\u00A0", "abc")
trim("15.5.4.20-4-27.js", "\u0009\u0009", "")
trim("15.5.4.20-4-28.js", "\u000B\u000B", "")
trim("15.5.4.20-4-29.js", "\u000C\u000C", "")
trim("15.5.4.20-4-30.js", "\u0020\u0020", "")
trim("15.5.4.20-4-32.js", "\u00A0\u00A0", "")
trim("15.5.4.20-4-34.js", "\uFEFF\uFEFF", "")
trim("15.5.4.20-4-35.js", "ab\u0009c", "ab\u0009c")
trim("15.5.4.20-4-36.js", "ab\u000Bc", "ab\u000Bc")
trim("15.5.4.20-4-37.js", "ab\u000Cc", "ab\u000Cc")
trim("15.5.4.20-4-38.js", "ab\u0020c", "ab\u0020c")
trim("15.5.4.20-4-39.js", "ab\u0085c", "ab\u0085c")
trim("15.5.4.20-4-40.js", "ab\u00A0c", "ab\u00A0c")
trim("15.5.4.20-4-41.js", "ab\u200Bc", "ab\u200Bc")
trim("15.5.4.20-4-42.js", "ab\uFEFFc", "ab\uFEFFc")
trim("15.5.4.20-4-43.js", "\u000Aabc", "abc")
trim("15.5.4.20-4-44.js", "\u000Dabc", "abc")
trim("15.5.4.20-4-45.js", "\u2028abc", "abc")
trim("15.5.4.20-4-46.js", "\u2029abc", "abc")
trim("15.5.4.20-4-47.js", "abc\u000A", "abc")
trim("15.5.4.20-4-48.js", "abc\u000D", "abc")
trim("15.5.4.20-4-49.js", "abc\u2028", "abc")
trim("15.5.4.20-4-50.js", "abc\u2029", "abc")
trim("15.5.4.20-4-51.js", "\u000Aabc\u000A", "abc")
trim("15.5.4.20-4-52.js", "\u000Dabc\u000D", "abc")
trim("15.5.4.20-4-53.js", "\u2028abc\u2028", "abc")
trim("15.5.4.20-4-54.js", "\u2029abc\u2029", "abc")
trim("15.5.4.20-4-55.js", "\u000A\u000A", "")
trim("15.5.4.20-4-56.js", "\u000D\u000D", "")
trim("15.5.4.20-4-57.js", "\u2028\u2028", "")
trim("15.5.4.20-4-58.js", "\u2029\u2029", "")
trim("15.5.4.20-4-59.js", "\u2029 abc", "abc")
trim("15.5.4.20-4-60.js", " ", "")
add("test/built-ins/String/prototype/trim/u180e.js", "trim", [
assertion("trailing U+180E", "_\u180E", "_\u180E"),
assertion("only U+180E", "\u180E", "\u180E"),
assertion("leading U+180E", "\u180E_", "\u180E_"),
])
const directionalWhitespace = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF"
add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, "a" + directionalWhitespace + "b" + directionalWhitespace),
])
add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, "a" + lineTerminators + "b" + lineTerminators),
])
add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, directionalWhitespace + "a" + directionalWhitespace + "b"),
])
add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, lineTerminators + "a" + lineTerminators + "b"),
])
add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [
assertion("repeat once", "abc", "abc", [1]),
assertion("repeat three times", "abc", "abcabcabc", [3]),
{ label: "repeat 10000 times length", input: ".", args: [10000], expected: 10000, outcome: "length" },
])
add("test/built-ins/String/prototype/repeat/empty-string-returns-empty.js", "repeat", [
assertion("count 1", "", "", [1]),
assertion("count 3", "", "", [3]),
assertion("maximum 32-bit count", "", "", [0xffffffff]),
])
add("test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.js", "repeat", [
assertion("zero", "foo", "", [0]),
])
add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [
assertion("fraction truncates to zero", "abc", "", [0.9]),
])
add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [assertion("empty fill", "abc", "abc", [5, ""])])
add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [
assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]),
assertion("single-character fill", "abc", "**abc", [5, "*"]),
assertion("truncated surrogate pair", "abc", "\uD83D\uDCA9\uD83Dabc", [6, "\uD83D\uDCA9"]),
])
add("test/built-ins/String/prototype/padStart/fill-string-omitted.js", "padStart", [
assertion("omitted fill", "abc", " abc", [5]),
assertion("undefined fill", "abc", " abc", [5, undefined]),
])
add("test/built-ins/String/prototype/padStart/max-length-not-greater-than-string.js", "padStart", [
assertion("NaN", "abc", "abc", [NaN, "def"]),
assertion("negative infinity", "abc", "abc", [-Infinity, "def"]),
assertion("zero", "abc", "abc", [0, "def"]),
assertion("negative one", "abc", "abc", [-1, "def"]),
assertion("equal length", "abc", "abc", [3, "def"]),
assertion("fraction truncates", "abc", "abc", [3.9999, "def"]),
])
add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [assertion("empty fill", "abc", "abc", [5, ""])])
add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [
assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]),
assertion("single-character fill", "abc", "abc**", [5, "*"]),
assertion("truncated surrogate pair", "abc", "abc\uD83D\uDCA9\uD83D", [6, "\uD83D\uDCA9"]),
])
add("test/built-ins/String/prototype/padEnd/fill-string-omitted.js", "padEnd", [
assertion("omitted fill", "abc", "abc ", [5]),
assertion("undefined fill", "abc", "abc ", [5, undefined]),
])
add("test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.js", "padEnd", [
assertion("NaN", "abc", "abc", [NaN, "def"]),
assertion("negative infinity", "abc", "abc", [-Infinity, "def"]),
assertion("zero", "abc", "abc", [0, "def"]),
assertion("negative one", "abc", "abc", [-1, "def"]),
assertion("equal length", "abc", "abc", [3, "def"]),
assertion("fraction truncates", "abc", "abc", [3.9999, "def"]),
])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [assertion("undefined position", "lego", "l", [undefined])])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [assertion("undefined position", "42", "4", [undefined])])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js", "charAt", ["A", "B", "C", "A", "B", "C"].map((expected, position) => assertion(`position ${position}`, "ABCABC", expected, [position])))
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js", "charAt", [-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])))
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js", "charAt", [6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])))
add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])])
add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [
assertion("positive zero", "abc", "a", [0]),
assertion("negative zero", "abc", "a", [-0]),
])
add("test/built-ins/String/prototype/charAt/pos-rounding.js", "charAt", [
assertion("-0.99999", "abc", "a", [-0.99999]),
assertion("-0.00001", "abc", "a", [-0.00001]),
assertion("0.00001", "abc", "a", [0.00001]),
assertion("0.99999", "abc", "a", [0.99999]),
assertion("1.00001", "abc", "b", [1.00001]),
assertion("1.99999", "abc", "b", [1.99999]),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [assertion("omitted position", "smart", 0x73)])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [assertion("undefined position", "lego", 0x6c, [undefined])])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [assertion("undefined position", "42", 0x34, [undefined])])
add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [
assertion("-0.99999", "abc", 0x61, [-0.99999]),
assertion("-0.00001", "abc", 0x61, [-0.00001]),
assertion("0.00001", "abc", 0x61, [0.00001]),
assertion("0.99999", "abc", 0x61, [0.99999]),
assertion("1.00001", "abc", 0x62, [1.00001]),
assertion("1.99999", "abc", 0x62, [1.99999]),
])
add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [
assertion("a", "abc", 97, [0]), assertion("b", "abc", 98, [1]), assertion("c", "abc", 99, [2]),
assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]), assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]),
assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]), assertion("trailing D800", "123\uD800", 0xd800, [3]),
assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]), assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]),
])
add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [
assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]),
assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]),
assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]), assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]),
assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]), assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]),
assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]), assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]),
assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]),
])
add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [
assertion("U+10000", "\uD800\uDC00", 65536, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]),
assertion("U+103FF", "\uD800\uDFFF", 66559, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]),
assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]), assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]),
assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]), assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]),
assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]),
])
add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [
assertion("NaN", "\uD800\uDC00", 65536, [NaN]), assertion("undefined", "\uD800\uDC00", 65536, [undefined]),
])
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [
{ label: "negative one", input: "abc", args: [-1], outcome: "undefined" },
{ label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" },
])
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js", "codePointAt", [
{ label: "equal to size", input: "abc", args: [3], outcome: "undefined" },
{ label: "greater than size", input: "abc", args: [4], outcome: "undefined" },
{ label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" },
])
add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [
assertion("position 0", "12\uD80034", "1", [0]), assertion("position 1", "12\uD80034", "2", [1]),
assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]), assertion("position 3", "12\uD80034", "3", [3]),
assertion("position 4", "12\uD80034", "4", [4]),
])
add("test/built-ins/String/prototype/at/returns-item.js", "at", ["1", "2", "3", "4", "5"].map((expected, position) => assertion(`position ${position}`, "12345", expected, [position])))
add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [
assertion("zero", "12345", "1", [0]), assertion("negative one", "12345", "5", [-1]),
assertion("negative three", "12345", "3", [-3]), assertion("negative four", "12345", "2", [-4]),
])
add("test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js", "at", [-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" })))
add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [assertion("undefined", "01", "0", [undefined])])
add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")])
add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [
assertion("empty string", "", ""), assertion("non-empty string", "str", "str"),
])
add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [
assertion("NFC short", "\u1E9B\u0323", "\u1E9B\u0323", ["NFC"]),
assertion("NFD short", "\u1E9B\u0323", "\u017F\u0323\u0307", ["NFD"]),
assertion("NFKC short", "\u1E9B\u0323", "\u1E69", ["NFKC"]),
assertion("NFKD short", "\u1E9B\u0323", "\u0073\u0323\u0307", ["NFKD"]),
assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]),
assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]),
assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]),
assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKD"]),
])
add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [
assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"),
assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [undefined]),
])
add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [
{ label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" },
{ label: "NFC1", input: "foo", args: ["NFC1"], outcome: "RangeError" },
])
add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [
assertion("D70", "o\u0308", 0, ["ö"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]),
assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]),
assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]),
assertion("Hangul", "\u1111\u1171\u11B6", 0, ["퓛"]), assertion("angstrom compatibility", "Å", 0, ["Å"]),
assertion("angstrom decomposed", "Å", 0, ["A\u030A"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]),
assertion("Vietnamese precomposed 1", "ự", 0, ["ụ\u031B"]), assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]),
assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]), assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]),
assertion("cedilla", "Ç", 0, ["C\u0327"]), assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]),
assertion("Hangul syllable", "가", 0, ["\u1100\u1161"]), assertion("ohm", "Ω", 0, ["Ω"]),
assertion("angstrom", "Å", 0, ["A\u030A"]), assertion("circumflex", "ô", 0, ["o\u0302"]),
assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]), assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]),
assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]),
])
add("test/built-ins/String/fromCharCode/S15.5.3.2_A2.js", "fromCharCode", [{ label: "no arguments", expected: "" }], true)
add("test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js", "fromCharCode", [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }], true)
add("test/built-ins/String/fromCharCode/S9.7_A1.js", "fromCharCode", [
{ label: "NaN", args: [NaN], expected: 0 }, { label: "zero", args: [0], expected: 0 }, { label: "negative zero", args: [-0], expected: 0 },
{ label: "positive infinity", args: [Infinity], expected: 0 }, { label: "negative infinity", args: [-Infinity], expected: 0 },
], true)
add("test/built-ins/String/fromCharCode/S9.7_A2.1.js", "fromCharCode", [
[0, 0], [1, 1], [-1, 65535], [65535, 65535], [65534, 65534], [65536, 0], [4294967295, 65535], [4294967294, 65534], [4294967296, 0],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true)
add("test/built-ins/String/fromCharCode/S9.7_A2.2.js", "fromCharCode", [
[-32767, 32769], [-32768, 32768], [-32769, 32767], [-65535, 1], [-65536, 0], [-65537, 65535], [65535, 65535], [65536, 0], [65537, 1], [131071, 65535], [131072, 0], [131073, 1],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true)
add("test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js", "fromCharCode", [
{ label: "positive fraction", args: [1.2345], expected: 1 }, { label: "negative fraction", args: [-5.4321], expected: 65531 },
], true)
add("test/built-ins/String/fromCodePoint/arguments-is-empty.js", "fromCodePoint", [{ label: "no arguments", expected: "" }], true)
add("test/built-ins/String/fromCodePoint/return-string-value.js", "fromCodePoint", [
{ label: "NUL", args: [0], expected: "\x00" }, { label: "asterisk", args: [42], expected: "*" },
{ label: "AZ", args: [65, 90], expected: "AZ" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" },
{ label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" }, { label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" },
{ label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" },
{ label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" },
], true)
add("test/built-ins/String/fromCodePoint/argument-is-not-integer.js", "fromCodePoint", [
{ label: "fraction", args: [3.14], outcome: "RangeError" }, { label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" },
], true)
add("test/built-ins/String/fromCodePoint/number-is-out-of-range.js", "fromCodePoint", [
{ label: "negative one", args: [-1], outcome: "RangeError" }, { label: "negative after valid", args: [1, -1], outcome: "RangeError" },
{ label: "above maximum", args: [1114112], outcome: "RangeError" }, { label: "infinity", args: [Infinity], outcome: "RangeError" },
], true)
describe("Test262-adapted core String behavior", () => {
for (const vector of vectors) {
test(vector.path, async () => {
const results = vector.assertions.map((item) => {
const args = (item.args ?? []).map(literal).join(", ")
const expression = vector.static
? `String.${vector.method}(${args})`
: `${JSON.stringify(item.input)}.${vector.method}(${args})`
const observed = vector.static && vector.method === "fromCharCode" && typeof item.expected === "number"
? `${expression}.charCodeAt(0)`
: expression
const checked = item.outcome === "undefined"
? `${observed} === undefined`
: item.outcome === "length"
? `${observed}.length`
: item.outcome === "RangeError"
? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()`
: observed
return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }`
})
const expected = vector.assertions.map((item) => ({
label: item.label,
value: item.outcome === undefined || item.outcome === "length" ? item.expected! : true,
}))
expect(await value(`return [${results.join(",")}]`)).toEqual(expected)
})
}
})
@@ -0,0 +1,625 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/String/prototype/split/separator-regexp.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js
* - test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js
* - test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js
* - test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js
* - test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js
* - test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js
* - test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js
* - test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js
* - test/built-ins/String/prototype/replace/regexp-capture-by-index.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js
* - test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js
* - test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js
* - test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js
* - test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js
* - test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js
* - test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js
* - test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js
* - test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js
* - test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js
*
* Copyright 2009 the Sputnik authors. All rights reserved.
* Copyright (C) 2019 Leo Balter. All rights reserved.
* Copyright (C) 2020 Rick Waldron. All rights reserved.
* Copyright (C) 2023 Richard Gibson. All rights reserved.
* Copyright (C) 2024 Tan Meng. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
type Vector = {
readonly path: string
readonly code: string
readonly expected: CodeMode.DataValue
}
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const run = (name: string, vectors: ReadonlyArray<Vector>) => {
describe(name, () => {
for (const vector of vectors) {
test(vector.path, async () => {
expect(await value(vector.code)).toEqual(vector.expected)
})
}
})
}
run("Test262-adapted regexp split behavior", [
{
path: "test/built-ins/String/prototype/split/separator-regexp.js",
code: `
return [
"x".split(/^/), "x".split(/$/), "x".split(/.?/), "x".split(/.*/), "x".split(/.+/),
"x".split(/.*?/), "x".split(/.{1}/), "x".split(/.{1,}/), "x".split(/.{1,2}/),
"x".split(/()/), "x".split(/./), "x".split(/(?:)/), "x".split(/(...)/),
"x".split(/(|)/), "x".split(/[]/), "x".split(/[^]/), "x".split(/[.-.]/),
"x".split(/\\0/), "x".split(/\\b/), "x".split(/\\B/), "x".split(/\\d/),
"x".split(/\\D/), "x".split(/\\n/), "x".split(/\\r/), "x".split(/\\s/),
"x".split(/\\S/), "x".split(/\\v/), "x".split(/\\w/), "x".split(/\\W/),
]
`,
expected: [
["x"], ["x"], ["", ""], ["", ""], ["", ""], ["x"], ["", ""], ["", ""], ["", ""],
["x"], ["", ""], ["x"], ["x"], ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"],
["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["", ""], ["x"], ["", ""], ["x"],
],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-s-and-3-and-instance-is-string-a-b-c-de-f.js",
code: `return "a b c de f".split(/\\s/, 3)`,
expected: ["a", "b", "c"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-s-and-instance-is-string-a-b-c-de-f.js",
code: `return "a b c de f".split(/\\s/)`,
expected: ["a", "b", "c", "de", "f"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-d-and-instance-is-string-dfe23iu-34-65.js",
code: `return "dfe23iu 34 =+65--".split(/\\d+/)`,
expected: ["dfe", "iu ", " =+", "--"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-reg-exp-d-and-instance-is-string-dfe23iu-34-65.js",
code: `return "dfe23iu 34 =+65--".split(new RegExp("\\\\d+"))`,
expected: ["dfe", "iu ", " =+", "--"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-a-z-and-instance-is-string-abc.js",
code: `return "abc".split(/[a-z]/)`,
expected: ["", "", "", ""],
},
{
path: "test/built-ins/String/prototype/split/argument-is-reg-exp-a-z-and-instance-is-string-abc.js",
code: `return "abc".split(new RegExp("[a-z]"))`,
expected: ["", "", "", ""],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-undefined-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, undefined)`,
expected: ["he", "", "o"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-0-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, 0)`,
expected: [],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-1-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, 1)`,
expected: ["he"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-2-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, 2)`,
expected: ["he", ""],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-3-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, 3)`,
expected: ["he", "", "o"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-regexp-l-and-4-and-instance-is-string-hello.js",
code: `return "hello".split(/l/, 4)`,
expected: ["he", "", "o"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-l-and-instance-is-string-hello.js",
code: `return "hello".split(/l/)`,
expected: ["he", "", "o"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-new-reg-exp-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp())`,
expected: ["h", "e", "l", "l", "o"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-0-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), 0)`,
expected: [],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-1-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), 1)`,
expected: ["h"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-2-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), 2)`,
expected: ["h", "e"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-3-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), 3)`,
expected: ["h", "e", "l"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-4-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), 4)`,
expected: ["h", "e", "l", "l"],
},
{
path: "test/built-ins/String/prototype/split/arguments-are-new-reg-exp-and-undefined-and-instance-is-string-hello.js",
code: `return "hello".split(new RegExp(), undefined)`,
expected: ["h", "e", "l", "l", "o"],
},
{
path: "test/built-ins/String/prototype/split/call-split-2-instance-is-string-one-two-three-four-five.js",
code: `return "one two three four five".split(/ /, 2)`,
expected: ["one", "two"],
},
{
path: "test/built-ins/String/prototype/split/separator-regexp-comma-instance-is-string-one-1-two-2-four-4.js",
code: `return "one-1,two-2,four-4".split(/,/)`,
expected: ["one-1", "two-2", "four-4"],
},
{
path: "test/built-ins/String/prototype/split/argument-is-regexp-x-and-instance-is-string-a-b-c-de-f.js",
code: `return "a b c de f".split(/X/)`,
expected: ["a b c de f"],
},
])
run("Test262-adapted replace behavior", [
{
path: "test/built-ins/String/prototype/replace/regexp-capture-by-index.js",
code: `
const str = "foo-x-bar"
const patterns = ["x", /x/, /(x)/, /(x)($^)?/, /((((((((((x))))))))))/]
const replacements = ["|$0|", "|$00|", "|$000|", "|$1|", "|$01|", "|$010|", "|$2|", "|$02|", "|$020|", "|$10|", "|$100|", "|$20|", "|$200|"]
return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement)))
`,
expected: [
"foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar",
"foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar",
"foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar",
"foo-|$1|-bar", "foo-|$1|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar",
"foo-|$01|-bar", "foo-|$01|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar",
"foo-|$010|-bar", "foo-|$010|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x0|-bar",
"foo-|$2|-bar", "foo-|$2|-bar", "foo-|$2|-bar", "foo-||-bar", "foo-|x|-bar",
"foo-|$02|-bar", "foo-|$02|-bar", "foo-|$02|-bar", "foo-||-bar", "foo-|x|-bar",
"foo-|$020|-bar", "foo-|$020|-bar", "foo-|$020|-bar", "foo-|0|-bar", "foo-|x0|-bar",
"foo-|$10|-bar", "foo-|$10|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x|-bar",
"foo-|$100|-bar", "foo-|$100|-bar", "foo-|x00|-bar", "foo-|x00|-bar", "foo-|x0|-bar",
"foo-|$20|-bar", "foo-|$20|-bar", "foo-|$20|-bar", "foo-|0|-bar", "foo-|x0|-bar",
"foo-|$200|-bar", "foo-|$200|-bar", "foo-|$200|-bar", "foo-|00|-bar", "foo-|x00|-bar",
],
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A1_T17.js",
code: `return "asdf".replace(new RegExp(undefined, "g"), "1")`,
expected: "1a1s1d1f1",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T1.js",
code: `return "She sells seashells by the seashore.".replace(/sh/g, "sch")`,
expected: "She sells seaschells by the seaschore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T2.js",
code: `return "She sells seashells by the seashore.".replace(/sh/g, "$$sch")`,
expected: "She sells sea$schells by the sea$schore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T3.js",
code: `return "She sells seashells by the seashore.".replace(/sh/g, "$&sch")`,
expected: "She sells seashschells by the seashschore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T4.js",
code: `return "She sells seashells by the seashore.".replace(/sh/g, "$\`sch")`,
expected: "She sells seaShe sells seaschells by the seaShe sells seashells by the seaschore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T5.js",
code: `return "She sells seashells by the seashore.".replace(/sh/g, "$'sch")`,
expected: "She sells seaells by the seashore.schells by the seaore.schore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T6.js",
code: `return "She sells seashells by the seashore.".replace(/sh/, "sch")`,
expected: "She sells seaschells by the seashore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T7.js",
code: `return "She sells seashells by the seashore.".replace(/sh/, "$$sch")`,
expected: "She sells sea$schells by the seashore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T8.js",
code: `return "She sells seashells by the seashore.".replace(/sh/, "$&sch")`,
expected: "She sells seashschells by the seashore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T9.js",
code: `return "She sells seashells by the seashore.".replace(/sh/, "$\`sch")`,
expected: "She sells seaShe sells seaschells by the seashore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A2_T10.js",
code: `return "She sells seashells by the seashore.".replace(/sh/, "$'sch")`,
expected: "She sells seaells by the seashore.schells by the seashore.",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T1.js",
code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`,
expected: "uid=115",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T2.js",
code: `return "uid=31".replace(/(uid=)(\\d+)/, "$1115")`,
expected: "uid=115",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A3_T3.js",
code: `return "uid=31".replace(/(uid=)(\\d+)/, "$11A15")`,
expected: "uid=1A15",
},
{
path: "test/built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js",
code: `return "aaaaaaaaaa,aaaaaaaaaaaaaaa".replace(/^(a+)\\1*,\\1+$/, "$1")`,
expected: "aaaaa",
},
])
run("Test262-adapted replaceAll behavior", [
{
path: "test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js",
code: `
return [
"abc abc abc".replaceAll(new RegExp("b", "g"), "z"),
"abc abc abc".replaceAll(new RegExp("b", "gy"), "z"),
"abc abc abc".replaceAll(new RegExp("b", "giy"), "z"),
"No Uppercase!".replaceAll(new RegExp("[A-Z]", "g"), ""),
"No Uppercase?".replaceAll(new RegExp("[A-Z]", "gy"), ""),
"NO UPPERCASE!".replaceAll(new RegExp("[A-Z]", "gy"), ""),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "$2-$1"),
"abcabcabcabc".replaceAll(new RegExp("(a(.))", "g"), "$1$2$3"),
"aabacadaeafagahaiajakalamano a azaya".replaceAll(new RegExp("(((((((((((((a(.).).).).).).).).))))))", "g"), "($10)-($12)-($1)"),
"abcba".replaceAll(new RegExp("b", "g"), "$'"),
"abcba".replaceAll(new RegExp("b", "g"), "$\`"),
"abcba".replaceAll(new RegExp("(?<named>b)", "g"), "($<named>)"),
"abcba".replaceAll(new RegExp("(?<named>b)", "g"), "($<named)"),
"abcba".replaceAll(new RegExp("(?<named>b)", "g"), "($<unnamed>)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$$)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$$)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$&)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$1)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$\`)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($$')"),
"abcabcabcabc".replaceAll(new RegExp("a(?<z>b)(ca)", "g"), "($$<z>)"),
"abcabcabcabc".replaceAll(new RegExp("a(b)(ca)", "g"), "($&)"),
]
`,
expected: [
"azc azc azc", "abc abc abc", "abc abc abc", "o ppercase!", "o Uppercase?", " UPPERCASE!",
"ca-bbcca-bbc", "abb$3cabb$3cabb$3cabb$3c",
"(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya",
"acbacaa", "aacabca", "a(b)c(b)a", "a($<named)c($<named)a", "a()c()a", "($)bc($)bc",
"($)bc($)bc", "($$)bc($$)bc", "($$)bc($$)bc", "($&)bc($&)bc", "($1)bc($1)bc",
"($`)bc($`)bc", "($')bc($')bc", "($<z>)bc($<z>)bc", "(abca)bc(abca)bc",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string.js",
code: `return ["aab c \\nx".replaceAll("", "_"), "a".replaceAll("", "_")]`,
expected: ["_a_a_b_ _c_ _ _\n_x_", "_a_"],
},
{
path: "test/built-ins/String/prototype/replaceAll/searchValue-empty-string-this-empty-string.js",
code: `return "".replaceAll("", "abc")`,
expected: "abc",
},
{
path: "test/built-ins/String/prototype/replaceAll/replaceValue-value-replaces-string.js",
code: `return ["aaab a a aac".replaceAll("aa", "z"), "aaab a a aac".replaceAll("aa", "a"), "aaab a a aac".replaceAll("a", "a"), "aaab a a aac".replaceAll("a", "z")]`,
expected: ["zab a a zc", "aab a a ac", "aaab a a aac", "zzzb z z zzc"],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js",
code: `
const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar."
return [str.replaceAll("ninguém", "$"), str.replaceAll("é", "$"), str.replaceAll("é", "$ -"), str.replaceAll("é", "$$$")]
`,
expected: [
"Ninguém é igual a $. Todo o ser humano é um estranho ímpar.",
"Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.",
"Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.",
"Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js",
code: `
const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar."
return [str.replaceAll("ninguém", "$$"), str.replaceAll("é", "$$"), str.replaceAll("é", "$$ -"), str.replaceAll("é", "$$&"), str.replaceAll("é", "$$$"), str.replaceAll("é", "$$$$")]
`,
expected: [
"Ninguém é igual a $. Todo o ser humano é um estranho ímpar.",
"Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.",
"Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.",
"Ningu$&m $& igual a ningu$&m. Todo o ser humano $& um estranho ímpar.",
"Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.",
"Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js",
code: `
const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar."
return [str.replaceAll("ninguém", "$&"), str.replaceAll("ninguém", "($&)"), str.replaceAll("é", "($&)"), str.replaceAll("é", "($&) $&")]
`,
expected: [
"Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar.",
"Ninguém é igual a (ninguém). Todo o ser humano é um estranho ímpar.",
"Ningu(é)m (é) igual a ningu(é)m. Todo o ser humano (é) um estranho ímpar.",
"Ningu(é) ém (é) é igual a ningu(é) ém. Todo o ser humano (é) é um estranho ímpar.",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js",
code: `
const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar."
return [str.replaceAll("ninguém", "$\`"), str.replaceAll("Ninguém", "$\`"), str.replaceAll("ninguém", "($\`)"), str.replaceAll("é", "($\`)")]
`,
expected: [
"Ninguém é igual a Ninguém é igual a . Todo o ser humano é um estranho ímpar.",
" é igual a ninguém. Todo o ser humano é um estranho ímpar.",
"Ninguém é igual a (Ninguém é igual a ). Todo o ser humano é um estranho ímpar.",
"Ningu(Ningu)m (Ninguém ) igual a ningu(Ninguém é igual a ningu)m. Todo o ser humano (Ninguém é igual a ninguém. Todo o ser humano ) um estranho ímpar.",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js",
code: `
const str = "Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar."
return [str.replaceAll("ninguém", "$'"), str.replaceAll(".", "--- $'"), str.replaceAll("é", "($')")]
`,
expected: [
"Ninguém é igual a . Todo o ser humano é um estranho ímpar.. Todo o ser humano é um estranho ímpar.",
"Ninguém é igual a ninguém--- Todo o ser humano é um estranho ímpar. Todo o ser humano é um estranho ímpar--- ",
"Ningu(m é igual a ninguém. Todo o ser humano é um estranho ímpar.)m ( igual a ninguém. Todo o ser humano é um estranho ímpar.) igual a ningu(m. Todo o ser humano é um estranho ímpar.)m. Todo o ser humano ( um estranho ímpar.) um estranho ímpar.",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js",
code: `
const str = "ABC AAA ABC AAA"
return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement))
`,
expected: ["$1 AAA $1 AAA", "$2 AAA $2 AAA", "$3 AAA $3 AAA", "$4 AAA $4 AAA", "$5 AAA $5 AAA", "$6 AAA $6 AAA", "$7 AAA $7 AAA", "$8 AAA $8 AAA", "$9 AAA $9 AAA"],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js",
code: `
const str = "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa"
return [str.replaceAll("a", "$11"), str.replaceAll("a", "$29")]
`,
expected: [
"$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11 $11$11$11$11$11$11$11$11$11$11$11$11$11$11$11$11",
"$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29 $29$29$29$29$29$29$29$29$29$29$29$29$29$29$29$29",
],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js",
code: `return "aaaaaaaaaaaaaaaa aaaaaaaa aaaaaaaaaaaaaaaa".replaceAll("a", "$<")`,
expected: "$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$< $<$<$<$<$<$<$<$<$<$<$<$<$<$<$<$<",
},
])
run("Test262-adapted match behavior", [
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A1_T14.js",
code: `const match = "ABBABABAB77BBAA".match(new RegExp("77")); return [match[0], match.index]`,
expected: ["77", 9],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T2.js",
code: `return "343443444".match(/34/g)`,
expected: ["34", "34", "34"],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T3.js",
code: `return "123456abcde7890".match(/\\d{1}/g)`,
expected: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T4.js",
code: `return "123456abcde7890".match(/\\d{2}/g)`,
expected: ["12", "34", "56", "78", "90"],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T5.js",
code: `return "123456abcde7890".match(/\\D{2}/g)`,
expected: ["ab", "cd"],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T6.js",
code: `const match = "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`,
expected: ["02134", "02134", true, 3, 14],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T7.js",
code: `return "Boston, Mass. 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`,
expected: ["02134"],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T8.js",
code: `const match = "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/); return [match[0], match[1], match[2] === undefined, match.length, match.index]`,
expected: ["02134", "02134", true, 3, 11],
},
{
path: "test/built-ins/String/prototype/match/S15.5.4.10_A2_T12.js",
code: `return "Boston, MA 02134".match(/([\\d]{5})([- ]?[\\d]{4})?$/g)`,
expected: ["02134"],
},
])
run("Test262-adapted matchAll behavior", [
{
path: "test/built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js",
code: `
const text = "𠮷a𠮷b𠮷"
const collect = (regex) => {
const matches = text.matchAll(regex)
return matches.map((match) => match[0]).concat(matches.map((match) => match.index))
}
const empty = text.matchAll(/(?:)/gu)
const complex = "a𠮷b􏿿c".matchAll(/\\P{ASCII}/gu)
return [
collect(/𠮷/g),
collect(/𠮷/gu),
collect(/\\p{Script=Han}/gu),
collect(/./gu),
empty.map((match) => match[0]).concat(empty.map((match) => match.index)).length,
complex.map((match) => match[0]),
]
`,
expected: [
["𠮷", "𠮷", "𠮷", 0, 3, 6],
["𠮷", "𠮷", "𠮷", 0, 3, 6],
["𠮷", "𠮷", "𠮷", 0, 3, 6],
["𠮷", "a", "𠮷", "b", "𠮷", 0, 2, 3, 5, 6],
12,
["𠮷", "􏿿"],
],
},
])
run("Test262-adapted search behavior", [
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A1_T14.js",
code: `return "ABBABABAB77BBAA".search(new RegExp("77"))`,
expected: 9,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T1.js",
code: `return "test string".search("string")`,
expected: 5,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T2.js",
code: `return "test string".search("String")`,
expected: -1,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T3.js",
code: `return "test string".search(/String/i)`,
expected: 5,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T4.js",
code: `return "one two three four five".search(/Four/)`,
expected: -1,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T5.js",
code: `return "one two three four five".search(/four/)`,
expected: 14,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T6.js",
code: `return "test string".search("notexist")`,
expected: -1,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A2_T7.js",
code: `return "test string probe".search("string pro")`,
expected: 5,
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T1.js",
code: `const text = "power of the power of the power of the great sword"; return [text.search(/the/), text.search(/the/g)]`,
expected: [9, 9],
},
{
path: "test/built-ins/String/prototype/search/S15.5.4.12_A3_T2.js",
code: `const text = "power of the power of the power of the great sword"; return [text.search(/of/), text.search(/of/g)]`,
expected: [6, 6],
},
])
@@ -0,0 +1,794 @@
/*
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js
* - test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js
* - test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js
* - test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js
* - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js
* - test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js
* - test/built-ins/String/prototype/split/call-split-instance-is-string.js
* - test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js
* - test/built-ins/String/prototype/split/instance-is-string.js
* - test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js
* - test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js
* - test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js
* - test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js
* - test/built-ins/String/prototype/split/separator-undef.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js
* - test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js
* - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js
* - test/annexB/built-ins/String/prototype/substr/start-negative.js
* - test/annexB/built-ins/String/prototype/substr/length-negative.js
* - test/annexB/built-ins/String/prototype/substr/length-positive.js
* - test/annexB/built-ins/String/prototype/substr/length-falsey.js
* - test/annexB/built-ins/String/prototype/substr/length-undef.js
* - test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js
* - test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js
* - test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js
* - test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js
* - test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js
* - test/built-ins/String/prototype/includes/String.prototype.includes_Success.js
* - test/built-ins/String/prototype/includes/searchstring-found-with-position.js
* - test/built-ins/String/prototype/includes/searchstring-found-without-position.js
* - test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js
* - test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js
* - test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js
* - test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js
* - test/built-ins/String/prototype/includes/coerced-values-of-position.js
* - test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js
* - test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js
* - test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js
* - test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js
* - test/built-ins/String/prototype/startsWith/out-of-bounds-position.js
* - test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js
* - test/built-ins/String/prototype/startsWith/coerced-values-of-position.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js
* - test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js
* - test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js
* - test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js
* - test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js
* - test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js
* - test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js
* - test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js
* - test/built-ins/String/prototype/endsWith/coerced-values-of-position.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js
* - test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js
* - test/built-ins/String/prototype/indexOf/position-tointeger.js
* - test/built-ins/String/prototype/indexOf/searchstring-tostring.js
* - test/built-ins/String/prototype/lastIndexOf/not-a-substring.js
*
* Copyright 2009 the Sputnik authors. All rights reserved.
* Copyright (c) 2014 Ryan Lewis. All rights reserved.
* Copyright (C) 2015 the V8 project authors. All rights reserved.
* Copyright (C) 2016 the V8 project authors. All rights reserved.
* Copyright (C) 2017 Josh Wolfe. All rights reserved.
* Copyright (C) 2020 Leo Balter. All rights reserved.
* Copyright (C) 2026 Garham Lee. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const cases = [
{
path: "test/built-ins/String/prototype/split/call-split-l-0-instance-is-string-hello.js",
code: `const result = "hello".split("l", 0); return [result.length, result[0] === undefined]`,
expected: [0, true],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[0] is expected to equal the value of __expected[0]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-1-instance-is-string-hello.js",
code: `const result = "hello".split("l", 1); return [result.length, result[0]]`,
expected: [1, "he"],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[0] is expected to equal the value of __expected[0]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-2-instance-is-string-hello.js",
code: `const result = "hello".split("l", 2); return [result.length, result[0], result[1]]`,
expected: [2, "he", ""],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[index] is expected to equal the value of __expected[index]",
"The value of __split[index] is expected to equal the value of __expected[index]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-3-instance-is-string-hello.js",
code: `const result = "hello".split("l", 3); return [result.length, result[0], result[1], result[2]]`,
expected: [3, "he", "", "o"],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[index] is expected to equal the value of __expected[index]",
"The value of __split[index] is expected to equal the value of __expected[index]",
"The value of __split[index] is expected to equal the value of __expected[index]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-4-instance-is-string-hello.js",
code: `const result = "hello".split("l", 4); return [result.length, result[0], result[1], result[2]]`,
expected: [3, "he", "", "o"],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[index] is expected to equal the value of __expected[index]",
"The value of __split[index] is expected to equal the value of __expected[index]",
"The value of __split[index] is expected to equal the value of __expected[index]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-na-n-instance-is-string-hello.js",
code: `const result = "hello".split("l", NaN); return [result.length, result[0] === undefined]`,
expected: [0, true],
labels: [
"The value of __split.length is expected to equal the value of __expected.length",
"The value of __split[0] is expected to equal the value of __expected[0]",
],
},
{
path: "test/built-ins/String/prototype/split/call-split-l-instance-is-string-hello.js",
code: `const result = "hello".split("l"); return [result.length, result[0], result[1], result[2]]`,
expected: [3, "he", "", "o"],
labels: [
"The value of __split.length is 3",
'The value of __split[0] is "he"',
'The value of __split[1] is ""',
'The value of __split[2] is "o"',
],
},
{
path: "test/built-ins/String/prototype/split/call-split-ll-instance-is-string-hello.js",
code: `const result = "hello".split("ll"); return [result.length, result[0], result[1]]`,
expected: [2, "he", "o"],
labels: ["The value of __split.length is 2", 'The value of __split[0] is "he"', 'The value of __split[1] is "o"'],
},
{
path: "test/built-ins/String/prototype/split/call-split-h-instance-is-string-hello.js",
code: `const result = "hello".split("h"); return [result.length, result[0], result[1]]`,
expected: [2, "", "ello"],
labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is "ello"'],
},
{
path: "test/built-ins/String/prototype/split/call-split-hello-instance-is-string-hello.js",
code: `const result = "hello".split("hello"); return [result.length, result[0], result[1]]`,
expected: [2, "", ""],
labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'],
},
{
path: "test/built-ins/String/prototype/split/call-split-hellothere-instance-is-string-hello.js",
code: `const result = "hello".split("hellothere"); return [result.length, result[0]]`,
expected: [1, "hello"],
labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'],
},
{
path: "test/built-ins/String/prototype/split/call-split-o-instance-is-string-hello.js",
code: `const result = "hello".split("o"); return [result.length, result[0], result[1]]`,
expected: [2, "hell", ""],
labels: ["The value of __split.length is 2", 'The value of __split[0] is "hell"', 'The value of __split[1] is ""'],
},
{
path: "test/built-ins/String/prototype/split/call-split-x-instance-is-string-hello.js",
code: `const result = "hello".split("x"); return [result.length, result[0]]`,
expected: [1, "hello"],
labels: ["The value of __split.length is 1", 'The value of __split[0] is "hello"'],
},
{
path: "test/built-ins/String/prototype/split/call-split-x-instance-is-empty-string.js",
code: `const result = "".split("x"); return [result.length, result[0]]`,
expected: [1, ""],
labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'],
},
{
path: "test/built-ins/String/prototype/split/call-split-4-instance-is-string-one-1-two-2-four-4.js",
code: `const result = "one-1 two-2 four-4".split("-4"); return [result.length, result[0], result[1]]`,
expected: [2, "one-1 two-2 four", ""],
labels: [
"The value of __split.length is 2",
'The value of __split[0] is "one-1 two-2 four"',
'The value of __split[1] is ""',
],
},
{
path: "test/built-ins/String/prototype/split/call-split-on-instance-is-string-one-1-two-2-four-4.js",
code: `const result = "one-1 two-2 four-4".split("on"); return [result.length, result[0], result[1]]`,
expected: [2, "", "e-1 two-2 four-4"],
labels: [
"The value of __split.length is 2",
'The value of __split[0] is ""',
'The value of __split[1] is "e-1 two-2 four-4"',
],
},
{
path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three-four-five.js",
code: `const result = "one two three four five".split(" "); return [result.length, ...result]`,
expected: [5, "one", "two", "three", "four", "five"],
labels: [
"The value of __split.length is 5", 'The value of __split[0] is "one"', 'The value of __split[1] is "two"',
'The value of __split[2] is "three"', 'The value of __split[3] is "four"', 'The value of __split[4] is "five"',
],
},
{
path: "test/built-ins/String/prototype/split/call-split-instance-is-string-one-two-three.js",
code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`,
expected: ["o", "n", "e", "e"],
labels: [
'The value of __split[0] is "o"', 'The value of __split[1] is "n"',
'The value of __split[11] is "e"', 'The value of __split[12] is "e"',
],
},
{
path: "test/built-ins/String/prototype/split/call-split-instance-is-string.js",
code: `const result = " ".split(" "); return [result.length, result[0], result[1]]`,
expected: [2, "", ""],
labels: ["The value of __split.length is 2", 'The value of __split[0] is ""', 'The value of __split[1] is ""'],
},
{
path: "test/built-ins/String/prototype/split/instance-is-string-one-two-three-four-five.js",
code: `const result = "one,two,three,four,five".split(); return [result.length, result[0]]`,
expected: [1, "one,two,three,four,five"],
labels: ["The value of __split.length is 1", 'The value of __split[0] is "one,two,three,four,five"'],
},
{
path: "test/built-ins/String/prototype/split/instance-is-string.js",
code: `const result = " ".split(); return [result.length, result[0]]`,
expected: [1, " "],
labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'],
},
{
path: "test/built-ins/String/prototype/split/separator-colon-instance-is-string-one-1-two-2-four-4.js",
code: `const result = "one-1,two-2,four-4".split(":"); return [result.length, result[0]]`,
expected: [1, "one-1,two-2,four-4"],
labels: ["The value of __split.length is 1", 'The value of __split[0] is "one-1,two-2,four-4"'],
},
{
path: "test/built-ins/String/prototype/split/separator-comma-instance-is-string-one-two-three-four-five.js",
code: `const result = "one,two,three,four,five".split(","); return [result.length, ...result]`,
expected: [5, "one", "two", "three", "four", "five"],
labels: [
"The value of __split.length is 5",
'The value of __split[0] is "one"',
'The value of __split[1] is "two"',
'The value of __split[2] is "three"',
'The value of __split[3] is "four"',
'The value of __split[4] is "five"',
],
},
{
path: "test/built-ins/String/prototype/split/separator-empty-string-instance-is-string.js",
code: `const result = " ".split(""); return [result.length, result[0]]`,
expected: [1, " "],
labels: ["The value of __split.length is 1", 'The value of __split[0] is " "'],
},
{
path: "test/built-ins/String/prototype/split/call-split-without-arguments-and-instance-is-empty-string.js",
code: `const result = "".split(); return [result.length, result[0]]`,
expected: [1, ""],
labels: ["The value of __split.length is 1", 'The value of __split[0] is ""'],
},
{
path: "test/built-ins/String/prototype/split/separator-undef.js",
code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`,
expected: [true, 1, "undefined is not a function"],
labels: ["implicit separator, result is array", "implicit separator, result.length", "implicit separator, [0] is the same string"],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js",
code: `return ["undefined".slice(undefined, 3)]`,
expected: ["und"],
labels: ['#1: new String("undefined").slice(x,3) === "und"'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T14.js",
code: `return ["report".slice(undefined)]`,
expected: ["report"],
labels: ['#1: "report".slice(function(){}()) === "report"'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T1.js",
code: `return [typeof "this is a string object".slice()]`,
expected: ["string"],
labels: ['#1: typeof __string.slice() === "string"'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T2.js",
code: `return ["this is a string object".slice(NaN, Infinity)]`,
expected: ["this is a string object"],
labels: ['#1: __string.slice(NaN, Infinity) === "this is a string object"'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T3.js",
code: `return ["".slice(1, 0)]`,
expected: [""],
labels: ['#1: __string.slice(1,0) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T4.js",
code: `return ["this is a string object".slice(Infinity, NaN)]`,
expected: [""],
labels: ['#1: __string.slice(Infinity, NaN) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T5.js",
code: `return ["this is a string object".slice(Infinity, Infinity)]`,
expected: [""],
labels: ['#1: __string.slice(Infinity, Infinity) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T6.js",
code: `return ["this is a string object".slice(-0.01, 0)]`,
expected: [""],
labels: ['#1: __string.slice(-0.01,0) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T7.js",
code: `const text = "this is a string object"; return [text.slice(text.length, text.length)]`,
expected: [""],
labels: ['#1: __string.slice(__string.length, __string.length) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T8.js",
code: `const text = "this is a string object"; return [text.slice(text.length + 1, 0)]`,
expected: [""],
labels: ['#1: __string.slice(__string.length+1, 0) === ""'],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A2_T9.js",
code: `return ["this is a string object".slice(-Infinity, -Infinity)]`,
expected: [""],
labels: ['#1: __string.slice(-Infinity, -Infinity) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T6.js",
code: `return ["undefined".substring(undefined, 3)]`,
expected: ["und"],
labels: ['#1: new String("undefined").substring(x,3) === "und"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A1_T14.js",
code: `return ["report".substring(undefined)]`,
expected: ["report"],
labels: ['#1: "report".substring(function(){}()) === "report"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T1.js",
code: `return [typeof "this is a string object".substring()]`,
expected: ["string"],
labels: ['#1: typeof __string.substring() === "string"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T2.js",
code: `return ["this is a string object".substring(NaN, Infinity)]`,
expected: ["this is a string object"],
labels: ['#1: __string.substring(NaN, Infinity) === "this is a string object"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T3.js",
code: `return ["".substring(1, 0)]`,
expected: [""],
labels: ['#1: __string.substring(1,0) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T4.js",
code: `return ["this is a string object".substring(Infinity, NaN)]`,
expected: ["this is a string object"],
labels: ['#1: __string.substring(Infinity, NaN) === "this is a string object"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T5.js",
code: `return ["this is a string object".substring(Infinity, Infinity)]`,
expected: [""],
labels: ['#1: __string.substring(Infinity, Infinity) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T6.js",
code: `return ["this is a string object".substring(-0.01, 0)]`,
expected: [""],
labels: ['#1: __string.substring(-0.01,0) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T7.js",
code: `const text = "this is a string object"; return [text.substring(text.length, text.length)]`,
expected: [""],
labels: ['#1: __string.substring(__string.length, __string.length) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js",
code: `const text = "this is a string object"; return [text.substring(text.length + 1, 0)]`,
expected: ["this is a string object"],
labels: ['#1: __string.substring(__string.length+1, 0) === "this is a string object"'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js",
code: `return ["this is a string object".substring(-Infinity, -Infinity)]`,
expected: [""],
labels: ['#1: __string.substring(-Infinity, -Infinity) === ""'],
},
{
path: "test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js",
code: `return ["this_is_a_string object".substring(0, 8)]`,
expected: ["this_is_"],
labels: ['#1: __string.substring(0,8) === "this_is_"'],
},
{
path: "test/annexB/built-ins/String/prototype/substr/start-negative.js",
code: `return ["abc".substr(-1), "abc".substr(-2), "abc".substr(-3), "abc".substr(-4), "abc".substr(-1.1)]`,
expected: ["c", "bc", "abc", "abc", "c"],
labels: ["-1", "-2", "-3", "size + intStart < 0", "floating point rounding semantics"],
},
{
path: "test/annexB/built-ins/String/prototype/substr/length-negative.js",
code: `return [
"abc".substr(0, -1), "abc".substr(0, -2), "abc".substr(0, -3), "abc".substr(0, -4),
"abc".substr(1, -1), "abc".substr(1, -2), "abc".substr(1, -3), "abc".substr(1, -4),
"abc".substr(2, -1), "abc".substr(2, -2), "abc".substr(2, -3), "abc".substr(2, -4),
"abc".substr(3, -1), "abc".substr(3, -2), "abc".substr(3, -3), "abc".substr(3, -4),
]`,
expected: ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
labels: [
"0, -1", "0, -2", "0, -3", "0, -4", "1, -1", "1, -2", "1, -3", "1, -4",
"2, -1", "2, -2", "2, -3", "2, -4", "3, -1", "3, -2", "3, -3", "3, -4",
],
},
{
path: "test/annexB/built-ins/String/prototype/substr/length-positive.js",
code: `return [
"abc".substr(0, 1), "abc".substr(0, 2), "abc".substr(0, 3), "abc".substr(0, 4),
"abc".substr(1, 1), "abc".substr(1, 2), "abc".substr(1, 3), "abc".substr(1, 4),
"abc".substr(2, 1), "abc".substr(2, 2), "abc".substr(2, 3), "abc".substr(2, 4),
"abc".substr(3, 1), "abc".substr(3, 2), "abc".substr(3, 3), "abc".substr(3, 4),
]`,
expected: ["a", "ab", "abc", "abc", "b", "bc", "bc", "bc", "c", "c", "c", "c", "", "", "", ""],
labels: [
"0, 1", "0, 1", "0, 1", "0, 1", "1, 1", "1, 1", "1, 1", "1, 1",
"2, 1", "2, 1", "2, 1", "2, 1", "3, 1", "3, 1", "3, 1", "3, 1",
],
},
{
path: "test/annexB/built-ins/String/prototype/substr/length-falsey.js",
code: `return ["abc".substr(0, NaN), "abc".substr(1, NaN), "abc".substr(2, NaN), "abc".substr(3, NaN)]`,
expected: ["", "", "", ""],
labels: ["start: 0, length: NaN", "start: 1, length: NaN", "start: 2, length: NaN", "start: 3, length: NaN"],
},
{
path: "test/annexB/built-ins/String/prototype/substr/length-undef.js",
code: `return [
"abc".substr(0), "abc".substr(1), "abc".substr(2), "abc".substr(3),
"abc".substr(0, undefined), "abc".substr(1, undefined), "abc".substr(2, undefined), "abc".substr(3, undefined),
]`,
expected: ["abc", "bc", "c", "", "abc", "bc", "c", ""],
labels: [
"start: 0, length: unspecified", "start: 1, length: unspecified", "start: 2, length: unspecified", "start: 3, length: unspecified",
"start: 0, length: undefined", "start: 1, length: undefined", "start: 2, length: undefined", "start: 3, length: undefined",
],
},
{
path: "test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js",
code: `return [
"\uD834\uDF06".substr(0), "\uD834\uDF06".substr(1), "\uD834\uDF06".substr(2),
"\uD834\uDF06".substr(0, 0), "\uD834\uDF06".substr(0, 1), "\uD834\uDF06".substr(0, 2),
]`,
expected: ["\uD834\uDF06", "\uDF06", "", "", "\uD834", "\uD834\uDF06"],
labels: ["start: 0", "start: 1", "start: 2", "end: 0", "end: 1", "end: 2"],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js",
code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js",
code: `return ["word".includes("w")]`, expected: [true], labels: ['"word".includes("w")'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js",
code: `return ["word".includes("w", 5)]`, expected: [false], labels: ['"word".includes("w", 5)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js",
code: `return ["word".includes("o", 3)]`, expected: [false], labels: ['"word".includes("o", 3)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js",
code: `return ["word".includes("w", 0)]`, expected: [true], labels: ['"word".includes("w", 0)'],
},
{
path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js",
code: `const text = "The future is cool!"; return [text.includes("The future", 0), text.includes(" is ", 1), text.includes("cool!", 10)]`,
expected: [true, true, true],
labels: [
'Returns true for str.includes("The future", 0)',
'Returns true for str.includes(" is ", 1)',
'Returns true for str.includes("cool!", 10)',
],
},
{
path: "test/built-ins/String/prototype/includes/searchstring-found-without-position.js",
code: `const text = "The future is cool!"; return [text.includes("The future"), text.includes("is cool!"), text.includes(text)]`,
expected: [true, true, true],
labels: [
'Returns true for str.includes("The future")',
'Returns true for str.includes("is cool!")',
"Returns true for str.includes(str)",
],
},
{
path: "test/built-ins/String/prototype/includes/searchstring-not-found-with-position.js",
code: `const text = "The future is cool!"; return [text.includes("The future", 1), text.includes(text, 1)]`,
expected: [false, false],
labels: ['Returns false on str.includes("The future", 1)', "Returns false on str.includes(str, 1)"],
},
{
path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`,
expected: [false, false], labels: ["Flash if not included", "includes is case sensitive"],
},
{
path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js",
code: `const text = "The future is cool!"; return [
text.includes("!", text.length + 1), text.includes("!", 100), text.includes("!", Infinity), text.includes("!", text.length),
]`,
expected: [false, false, false, false],
labels: [
'str.includes("!", str.length + 1) returns false', 'str.includes("!", 100) returns false',
'str.includes("!", Infinity) returns false', 'str.includes("!", str.length) returns false',
],
},
{
path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`,
expected: [true, true, true],
labels: ['str.includes("", str.length) returns true', 'str.includes("") returns true', 'str.includes("", Infinity) returns true'],
},
{
path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js",
code: `const text = "The future is cool!"; return [
text.includes("The future", NaN), text.includes("The future", undefined), text.includes("The future", 0.4),
text.includes("The future", -1), text.includes("The future", 1.4),
]`,
expected: [true, true, true, true, false],
labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "negative position", "1.4 coerced to 1"],
},
{
path: "test/built-ins/String/prototype/startsWith/searchstring-found-with-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`,
expected: [true, true, true],
labels: [
'str.startsWith("The future", 0) === true', 'str.startsWith("future", 4) === true',
'str.startsWith(" is cool!", 10) === true',
],
},
{
path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`,
expected: [true, true, true],
labels: ['str.startsWith("The ") === true', 'str.startsWith("The future") === true', "str.startsWith(str) === true"],
},
{
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("The future", 1), text.startsWith(text, 1)]`,
expected: [false, false],
labels: ['str.startsWith("The future", 1) === false', "str.startsWith(str, 1) === false"],
},
{
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`,
expected: [false, false, false],
labels: ['str.startsWith("Flash") === false', "startsWith is case sensitive", 'str.startsWith("future is cool!") === false'],
},
{
path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js",
code: `const text = "The future is cool!"; return [
text.startsWith("!", text.length), text.startsWith("!", 100), text.startsWith("!", Infinity),
text.startsWith("The future", -1), text.startsWith("The future", -Infinity),
]`,
expected: [false, false, false, true, true],
labels: [
'str.startsWith("!", str.length) returns false', 'str.startsWith("!", 100) returns false',
'str.startsWith("!", Infinity) returns false', "position argument < 0 will search from the start of the string (-1)",
"position argument < 0 will search from the start of the string (-Infinity)",
],
},
{
path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`,
expected: [true, true, true],
labels: ['str.startsWith("") returns true', 'str.startsWith("", str.length) returns true', 'str.startsWith("", Infinity) returns true'],
},
{
path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js",
code: `const text = "The future is cool!"; return [
text.startsWith("The future", NaN), text.startsWith("The future", undefined),
text.startsWith("The future", 0.4), text.startsWith("The future", 1.4),
]`,
expected: [true, true, true, false],
labels: ["NaN coerced to 0", "undefined coerced to 0", "0.4 coerced to 0", "1.4 coerced to 1"],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js",
code: `return ["word".endsWith("d")]`, expected: [true], labels: ['"word".endsWith("d")'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js",
code: `return ["word".endsWith("d", 4)]`, expected: [true], labels: ['"word".endsWith("d", 4)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js",
code: `return ["word".endsWith("d", 25)]`, expected: [true], labels: ['"word".endsWith("d", 25)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js",
code: `return ["word".endsWith("r", 3)]`, expected: [true], labels: ['"word".endsWith("r", 3)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js",
code: `return ["word".endsWith("r")]`, expected: [false], labels: ['"word".endsWith("r")'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js",
code: `return ["word".endsWith("d", 3)]`, expected: [false], labels: ['"word".endsWith("d", 3)'],
},
{
path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`,
expected: [true, true, true],
labels: [
'str.endsWith("The future", 10) === true', 'str.endsWith("future", 10) === true',
'str.endsWith(" is cool!", str.length) === true',
],
},
{
path: "test/built-ins/String/prototype/endsWith/searchstring-found-without-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("cool!"), text.endsWith("!"), text.endsWith(text)]`,
expected: [true, true, true],
labels: ['str.endsWith("cool!") === true', 'str.endsWith("!") === true', "str.endsWith(str) === true"],
},
{
path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-with-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("is cool!", text.length - 1), text.endsWith("!", 1)]`,
expected: [false, false],
labels: ['str.endsWith("is cool!", str.length - 1) === false', 'str.endsWith("!", 1) === false'],
},
{
path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`,
expected: [false, false, false],
labels: ['str.endsWith("is Flash!") === false', "endsWith is case sensitive", 'str.endsWith("The future") === false'],
},
{
path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js",
code: `return ["web".endsWith("w", 0), "Bob".endsWith(" Bob")]`,
expected: [false, false],
labels: ['"web".endsWith("w", 0) returns false', '"Bob".endsWith(" Bob") returns false'],
},
{
path: "test/built-ins/String/prototype/endsWith/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [
text.endsWith(""), text.endsWith("", text.length), text.endsWith("", Infinity),
text.endsWith("", -1), text.endsWith("", -Infinity),
]`,
expected: [true, true, true, true, true],
labels: [
'str.endsWith("") returns true', 'str.endsWith("", str.length) returns true', 'str.endsWith("", Infinity) returns true',
'str.endsWith("", -1) returns true', 'str.endsWith("", -Infinity) returns true',
],
},
{
path: "test/built-ins/String/prototype/endsWith/coerced-values-of-position.js",
code: `const text = "The future is cool!"; return [
text.endsWith("", NaN), text.endsWith("", undefined), text.endsWith("The future", 10.4),
]`,
expected: [true, true, true],
labels: ["NaN coerced to 0", "undefined coerced to 0", "10.4 coerced to 10"],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js",
code: `return ["abcd".indexOf("abcdab")]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab")===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js",
code: `return ["abcd".indexOf("abcdab", 0)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",0)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js",
code: `return ["abcd".indexOf("abcdab", 99)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",99)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js",
code: `return ["abcd".indexOf("abcdab", NaN)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js",
code: `return ["$$abcdabcd".indexOf("ab", NaN)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js",
code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'],
},
{
path: "test/built-ins/String/prototype/indexOf/position-tointeger.js",
code: `return [
"aaaa".indexOf("aa", 0), "aaaa".indexOf("aa", 1), "aaaa".indexOf("aa", -0.9),
"aaaa".indexOf("aa", 0.9), "aaaa".indexOf("aa", 1.9), "aaaa".indexOf("aa", NaN),
"aaaa".indexOf("aa", Infinity), "aaaa".indexOf("aa", undefined),
"aaaa".indexOf("aa", 2), "aaaa".indexOf("aa", 2.9),
]`,
expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2],
labels: [
"position 0", "position 1", "ToInteger: truncate towards 0 (-0.9)", "ToInteger: truncate towards 0 (0.9)",
"ToInteger: truncate towards 0 (1.9)", "ToInteger: NaN => 0", "position Infinity",
"ToInteger: undefined => NaN => 0", "position 2", "ToInteger: truncate towards 0 (2.9)",
],
},
{
path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js",
code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`,
expected: [0, 2], labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'],
},
{
path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js",
code: `return ["abc".lastIndexOf("d")]`,
expected: [-1],
labels: ["String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this."],
},
] as const
describe("Test262-adapted String search and extraction behavior", () => {
for (const item of cases) {
test(item.path, async () => {
const actual = await value(item.code)
if (!Array.isArray(actual)) throw new Error(`expected assertion values for ${item.path}`)
expect(actual.length, "adapted assertion count").toBe(item.expected.length)
item.expected.forEach((expected, index) => expect(actual[index], item.labels[index]!).toEqual(expected))
})
}
})
+77
View File
@@ -0,0 +1,77 @@
# Test262 Array Coverage
The Array tests adapt Test262 at revision `250f204f23a9249ff204be2baec29600faae7b75`. They cover CodeMode's 35
exposed instance methods and three static methods using actual arrays, accepted argument types, deterministic behavior,
and CodeMode's materialized collection conventions. Each executable case names its exact upstream source path.
`LICENSE.test262` contains the upstream BSD terms.
This is coverage of CodeMode's bounded Array surface, not a claim of ECMAScript or Test262 conformance. One upstream
file may contain both adapted and inapplicable assertions, so a cited source means only that the represented assertions
were adapted.
## Inventory
The 38 relevant upstream API directories contain 2,837 files. The executable suite adapts assertions from 83 distinct
sources.
| API | Upstream files | Adapted sources |
| ------------------------------- | -------------: | --------------: |
| `Array.prototype.map` | 216 | 3 |
| `Array.prototype.filter` | 242 | 3 |
| `Array.prototype.find` | 23 | 4 |
| `Array.prototype.findIndex` | 23 | 3 |
| `Array.prototype.findLast` | 24 | 3 |
| `Array.prototype.findLastIndex` | 24 | 3 |
| `Array.prototype.some` | 219 | 2 |
| `Array.prototype.every` | 218 | 2 |
| `Array.prototype.includes` | 30 | 2 |
| `Array.prototype.join` | 23 | 2 |
| `Array.prototype.reduce` | 260 | 3 |
| `Array.prototype.reduceRight` | 260 | 3 |
| `Array.prototype.flatMap` | 24 | 2 |
| `Array.prototype.forEach` | 190 | 2 |
| `Array.prototype.sort` | 54 | 3 |
| `Array.prototype.toSorted` | 21 | 4 |
| `Array.prototype.slice` | 71 | 1 |
| `Array.prototype.concat` | 69 | 3 |
| `Array.prototype.indexOf` | 201 | 2 |
| `Array.prototype.lastIndexOf` | 198 | 2 |
| `Array.prototype.at` | 13 | 3 |
| `Array.prototype.flat` | 19 | 2 |
| `Array.prototype.reverse` | 18 | 1 |
| `Array.prototype.toReversed` | 17 | 2 |
| `Array.prototype.with` | 21 | 2 |
| `Array.prototype.push` | 24 | 1 |
| `Array.prototype.pop` | 23 | 1 |
| `Array.prototype.shift` | 20 | 1 |
| `Array.prototype.unshift` | 22 | 1 |
| `Array.prototype.splice` | 81 | 3 |
| `Array.prototype.fill` | 22 | 3 |
| `Array.prototype.copyWithin` | 39 | 2 |
| `Array.prototype.keys` | 12 | 1 |
| `Array.prototype.values` | 12 | 1 |
| `Array.prototype.entries` | 12 | 1 |
| `Array.from` | 47 | 3 |
| `Array.isArray` | 29 | 2 |
| `Array.of` | 16 | 1 |
## Exclusions
Assertions are not adapted when they test behavior outside CodeMode's documented Array surface:
- Function metadata, property descriptors, constructibility, prototype mutation, species constructors, or cross-realm
identity.
- Generic receivers, detached methods, `.call`, `.apply`, boxed values, custom coercion objects, Symbols, BigInts,
proxies, accessors, frozen arrays, typed arrays, or ArrayBuffers.
- `Array.from` mappers, custom iterables, constructor substitution, and iterator-closing behavior.
- Native iterator identity, `.next()`, completion records, or live iterator mutation. CodeMode deliberately materializes
`keys`, `values`, and `entries` as arrays.
- Sparse-array assertions that depend on literal elisions or inherited indexed properties. CodeMode's confined data
model does not preserve those prototype and hole semantics at every boundary.
- Argument coercions outside the accepted schema-like surface. Numeric positions must be numbers and `join` separators
must be strings.
- Exact native error brands where CodeMode exposes a safe runtime error instead.
- Async/effectful callbacks, circular-data rejection, sandbox-value identity, diagnostics, and host-boundary behavior.
Those remain covered by CodeMode-specific tests.
Handwritten tests remain where they specify CodeMode behavior rather than ordinary ECMAScript Array semantics.
+40
View File
@@ -0,0 +1,40 @@
# Test262 Promise Coverage
The Promise tests adapt Test262 at revision `250f204f23a9249ff204be2baec29600faae7b75`. The upstream Promise tree
contains 729 files; 459 are under the eight API directories corresponding to CodeMode's five Promise statics and three
chaining methods. The executable suite currently cites 39 distinct Promise, Array, and async-function sources.
This is coverage of CodeMode's confined Promise surface, not a claim of ECMAScript or Test262 conformance. One upstream
file may contain both adapted and inapplicable assertions, so a cited source means only that the represented assertions
were adapted. `LICENSE.test262` contains the upstream BSD terms.
## Covered Surface
- `Promise.all`, `Promise.allSettled`, and `Promise.race` result types, ordering, sparse positions, mixed values, and
rejection propagation.
- `Promise.resolve` value adoption and promise identity.
- `Promise.reject` reason preservation.
- Async-function promise creation, returned-value adoption, and throws before and after `await`.
The Promise PR's handwritten tests remain authoritative for tool-call concurrency, execution-scoped lifetime,
cancellation, output boundaries, model-safe tool failures, unhandled-rejection diagnostics, and chaining behavior.
## Exclusions
- `new Promise`, externally captured resolving functions, and executor semantics.
- Promise subclasses, constructors, species, realms, proxies, property descriptors, and function metadata.
- Custom thenables, poisoned `then` accessors, and arbitrary iterable or iterator-closing behavior.
- `Promise.any`, `Promise.try`, `Promise.withResolvers`, `Promise.allKeyed`, and `Promise.allSettledKeyed`.
- Exact native microtask behavior for `.then`, `.catch`, and `.finally`; those are exercised separately by the Promise
PR's chaining tests and audit.
- Exact native unhandled-rejection timing. CodeMode drains execution-owned work and reports safe diagnostics at its
execution boundary.
## Observed Differences
- Promise combinators accept arrays plus CodeMode's supported spreadable collections. The model guidance currently
describes arrays, so this remains a contract decision rather than conformance coverage.
- `Promise.race([])` returns an actionable error instead of a permanently pending promise, which cannot usefully finish
a bounded CodeMode execution.
- Promise identity is preserved, but CodeMode intentionally rejects general binary operators over Promise references;
identity is tested through `Map` keys instead of `===`.
+69
View File
@@ -0,0 +1,69 @@
# Test262 String Coverage
The String tests adapt Test262 at revision `250f204f23a9249ff204be2baec29600faae7b75`. They cover CodeMode's 32
exposed instance methods and two static methods using primitive receivers, accepted argument types, and deterministic
behavior. Each executable case names its exact upstream source path. `LICENSE.test262` contains the upstream BSD terms.
This is coverage of CodeMode's bounded String surface, not a claim of ECMAScript or Test262 conformance. One upstream
file may contain both adapted and inapplicable assertions, so a cited source means only that the represented assertions
were adapted.
## Inventory
The relevant upstream directories contain 1,048 files: 1,009 core built-in files, 29 Annex B files for exposed methods,
and 10 Intl `localeCompare` files. The executable suite adapts assertions from 298 distinct sources.
| API | Upstream files | Adapted sources |
| --- | ---: | ---: |
| `String.fromCharCode` | 17 | 6 |
| `String.fromCodePoint` | 11 | 4 |
| `String.prototype.at` | 11 | 5 |
| `String.prototype.charAt` | 30 | 9 |
| `String.prototype.charCodeAt` | 25 | 4 |
| `String.prototype.codePointAt` | 16 | 6 |
| `String.prototype.concat` | 22 | 1 |
| `String.prototype.endsWith` | 27 | 13 |
| `String.prototype.includes` | 27 | 12 |
| `String.prototype.indexOf` | 47 | 8 |
| `String.prototype.lastIndexOf` | 25 | 1 |
| `String.prototype.localeCompare` | 23 | 1 |
| `String.prototype.match` | 52 | 9 |
| `String.prototype.matchAll` | 26 | 1 |
| `String.prototype.normalize` | 14 | 3 |
| `String.prototype.padEnd` | 13 | 4 |
| `String.prototype.padStart` | 13 | 4 |
| `String.prototype.repeat` | 16 | 4 |
| `String.prototype.replace` | 56 | 16 |
| `String.prototype.replaceAll` | 46 | 12 |
| `String.prototype.search` | 44 | 10 |
| `String.prototype.slice` | 38 | 11 |
| `String.prototype.split` | 121 | 50 |
| `String.prototype.startsWith` | 21 | 7 |
| `String.prototype.substr` | 15 | 6 |
| `String.prototype.substring` | 46 | 12 |
| `String.prototype.toLowerCase` | 30 | 5 |
| `String.prototype.toString` | 7 | 1 |
| `String.prototype.toUpperCase` | 26 | 3 |
| `String.prototype.trim` | 129 | 66 |
| `String.prototype.trimEnd` | 23 | 2 |
| `String.prototype.trimLeft` | 4 | 0 |
| `String.prototype.trimRight` | 4 | 0 |
| `String.prototype.trimStart` | 23 | 2 |
## Exclusions
Assertions are not adapted when they test behavior outside CodeMode's documented String surface:
- Function metadata, property descriptors, constructibility, prototype mutation, or cross-realm identity.
- The `trimLeft`/`trimRight` Test262 files assert prototype function identity, which CodeMode does not expose. Their
supported call behavior remains covered by CodeMode-specific tests.
- Boxed strings, generic receivers, custom coercion objects, Symbols, BigInts, or argument types CodeMode rejects.
- Symbol-based RegExp dispatch, custom matchers, species constructors, or iterator protocol details. CodeMode materializes
`matchAll` results instead of exposing iterators.
- Locale selection and options. CodeMode deliberately uses the host default locale and ignores those arguments.
- Test262 harness behavior or setup syntax unavailable in the confined interpreter.
- Function-replacer behavior that is covered by CodeMode-specific tests for sequential callbacks, async tool calls,
result coercion, diagnostics, and sandbox boundaries.
- Assertions requiring an exact native error type when CodeMode deliberately exposes only its safe runtime error.
Handwritten tests remain where they specify CodeMode behavior rather than ordinary ECMAScript String semantics.
+2
View File
@@ -8,6 +8,8 @@ import { State } from "./state"
export const ID = Agent.ID
export type ID = typeof ID.Type
export const Name = Agent.Name
export type Name = Agent.Name
export const defaultID = ID.make("build")
export const Color = Agent.Color
+7 -3
View File
@@ -6,6 +6,7 @@ import type {
JSONValue,
LanguageModelV3,
LanguageModelV3CallOptions,
LanguageModelV3FinishReason,
LanguageModelV3FunctionTool,
LanguageModelV3Message,
LanguageModelV3Prompt,
@@ -304,6 +305,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const route: AnyRoute = {
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
provider: ProviderID.make(info.providerID),
providerMetadataKey: optionKey,
protocol: "ai-sdk",
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
auth: Auth.none,
@@ -416,7 +418,7 @@ function assistantPart(part: ContentPart): AssistantContent {
case "media":
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
case "reasoning":
return [{ type: "reasoning", text: part.text }]
return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }]
case "tool-call":
return [
{
@@ -425,6 +427,7 @@ function assistantPart(part: ContentPart): AssistantContent {
toolName: part.name,
input: part.input,
providerExecuted: part.providerExecuted,
providerOptions: providerOptions(part.providerMetadata),
},
]
case "tool-result":
@@ -440,6 +443,7 @@ function toolResultPart(part: ContentPart): ToolResultContent[] {
toolCallId: part.id,
toolName: part.name,
output: toolOutput(part.result),
providerOptions: providerOptions(part.providerMetadata),
},
]
}
@@ -624,8 +628,8 @@ function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["us
return Object.values(output).some((value) => value !== undefined) ? output : undefined
}
function finishReason(value: unknown): FinishReason {
return Schema.is(FinishReason)(value) ? value : "unknown"
function finishReason(value: LanguageModelV3FinishReason): FinishReason {
return value.unified === "other" ? "unknown" : value.unified
}
function providerMetadata(value: unknown) {
+40 -6
View File
@@ -119,7 +119,17 @@ export class Directory extends Schema.Class<Directory>("Config.Directory")({
path: AbsolutePath,
}) {}
export type Entry = Document | Directory
export class AgentsDirectory extends Schema.Class<AgentsDirectory>("Config.AgentsDirectory")({
type: Schema.Literal("agents"),
path: AbsolutePath,
}) {}
export class ClaudeDirectory extends Schema.Class<ClaudeDirectory>("Config.ClaudeDirectory")({
type: Schema.Literal("claude"),
path: AbsolutePath,
}) {}
export type Entry = Document | Directory | AgentsDirectory | ClaudeDirectory
export function latest<K extends keyof Info>(entries: readonly Entry[], key: K): Info[K] | undefined {
return entries
@@ -176,16 +186,37 @@ const layer = Layer.effect(
const discover = Effect.fn("Config.discover")(function* () {
const globalDirectory = AbsolutePath.make(global.config)
const globalAgentsDirectory = AbsolutePath.make(path.join(global.home, ".agents"))
const globalClaudeDirectory = AbsolutePath.make(path.join(global.home, ".claude"))
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
const discovered = locationIsGlobal
? []
: yield* fs
.up({
targets: [".opencode", ...names.toReversed()],
targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
start: location.directory,
stop: location.project.directory,
})
.pipe(Effect.orDie)
.pipe(Effect.orDie)
// We load certain files from a few other folders in the ecosystem
const claude = [
...((yield* fs.isDir(globalClaudeDirectory))
? [new ClaudeDirectory({ type: "claude", path: globalClaudeDirectory })]
: []),
...discovered
.filter((item) => path.basename(item) === ".claude")
.map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) })),
]
const agents = [
...((yield* fs.isDir(globalAgentsDirectory))
? [new AgentsDirectory({ type: "agents", path: globalAgentsDirectory })]
: []),
...discovered
.filter((item) => path.basename(item) === ".agents")
.map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) })),
]
const directories = [
globalDirectory,
...discovered
@@ -193,15 +224,18 @@ const layer = Layer.effect(
.toReversed()
.map((directory) => AbsolutePath.make(directory)),
]
const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
const directPaths = discovered
.filter((item) => ![".agents", ".claude", ".opencode"].includes(path.basename(item)))
.toReversed()
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
Effect.orDie,
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
)
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return {
entries: [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()],
directories,
entries: [...claude, ...agents, ...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()],
directories: [...directories, ...claude.map((entry) => entry.path), ...agents.map((entry) => entry.path)],
files: directPaths,
}
})
+1
View File
@@ -40,6 +40,7 @@ export const Plugin = define({
const load = Effect.fn("ConfigAgentPlugin.load")(function* () {
return yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([entry])
if (entry.type !== "directory") return Effect.succeed([])
return Effect.gen(function* () {
const files = yield* discover(fs, entry.path)
return yield* Effect.forEach(files, (file) =>
@@ -19,6 +19,7 @@ export const Plugin = define({
const load = Effect.fn("ConfigCommandPlugin.load")(function* () {
return yield* Effect.forEach(yield* config.entries(), (entry) => {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
if (entry.type !== "directory") return Effect.succeed([])
return loadDirectory(fs, entry.path).pipe(
Effect.map((commands) => [
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
+3 -2
View File
@@ -1,6 +1,7 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"
@@ -91,8 +92,8 @@ export const Plugin = define({
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
},
}))
}
+10
View File
@@ -17,8 +17,18 @@ export const Plugin = define({
const location = yield* Location.Service
const loaded = { entries: yield* config.entries() }
yield* ctx.skill.transform((draft) => {
const claude = loaded.entries.flatMap((entry) => (entry.type === "claude" ? [entry.path] : []))
const agents = loaded.entries.flatMap((entry) => (entry.type === "agents" ? [entry.path] : []))
const directories = loaded.entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
for (const directory of [...claude, ...agents]) {
draft.source(
SkillV2.DirectorySource.make({
type: "directory",
path: AbsolutePath.make(path.join(directory, "skills")),
}),
)
}
for (const directory of directories) {
draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
+5 -4
View File
@@ -1,6 +1,7 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { ModelV2 } from "../model"
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@@ -17,8 +18,8 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
read: Schema.Finite.pipe(Schema.optional),
write: Schema.Finite.pipe(Schema.optional),
read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional),
}) {}
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
@@ -26,8 +27,8 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
type: Schema.Literal("context"),
size: Schema.Int,
}).pipe(Schema.optional),
input: Schema.Finite,
output: Schema.Finite,
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache: Cache.pipe(Schema.optional),
}) {}
+1
View File
@@ -48,5 +48,6 @@ export const migrations = (
import("./migration/20260705180000_rename_instructions"),
import("./migration/20260706223930_add-session-fork"),
import("./migration/20260707010146_durable_session_inbox"),
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,227 @@
import { sql } from "drizzle-orm"
import { Effect, Schema } from "effect"
import type { DatabaseMigration } from "../migration"
const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)
const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown))
export default {
id: "20260707120000_migrate_prelaunch_v2_state",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(
sql`DELETE FROM session_message WHERE type = 'compaction' AND json_extract(data, '$.status') = 'queued'`,
)
const messages = yield* tx.all<{ id: string; type: string; data: string }>(
sql`SELECT id, type, data FROM session_message WHERE type IN ('skill', 'shell', 'assistant', 'compaction', 'synthetic')`,
)
for (const row of messages) {
const data = object(decodeJson(row.data))
yield* tx.run(
sql`UPDATE session_message SET data = ${JSON.stringify(messageData(row.type, data))} WHERE id = ${row.id}`,
)
}
yield* tx.run(sql`DELETE FROM event WHERE type = 'session.compaction.delta.1'`)
const events = yield* tx.all<{ id: string; aggregateID: string; seq: number; type: string; data: string }>(sql`
SELECT id, aggregate_id as aggregateID, seq, type, data
FROM event
WHERE type IN (
'session.skill.activated.1',
'session.skill.activated.2',
'session.compaction.started.1',
'session.compaction.started.2',
'session.compaction.ended.1',
'session.compaction.failed.1',
'session.compaction.failed.2',
'session.revert.staged.1',
'session.revert.staged.2'
)
ORDER BY aggregate_id, seq
`)
const compactionReasons = new Map<string, "auto" | "manual">()
for (const row of events) {
const data = object(decodeJson(row.data))
if (row.type.startsWith("session.compaction.ended.")) {
compactionReasons.delete(row.aggregateID)
continue
}
const event = eventData(row.type, data, compactionReasons.get(row.aggregateID))
if (row.type.startsWith("session.compaction.started."))
compactionReasons.set(row.aggregateID, event.data.reason === "auto" ? "auto" : "manual")
if (row.type.startsWith("session.compaction.failed.")) compactionReasons.delete(row.aggregateID)
yield* tx.run(
sql`UPDATE event SET type = ${event.type}, data = ${JSON.stringify(event.data)} WHERE id = ${row.id}`,
)
}
})
},
} satisfies DatabaseMigration.Migration
function messageData(type: string, data: Record<string, unknown>) {
if (type === "skill")
return defined({
metadata: data.metadata,
time: data.time,
skill: data.skill ?? data.id ?? data.name,
name: data.name,
text: data.text,
})
if (type === "shell") {
const shell = object(data.shell)
return defined({
metadata: data.metadata,
time: data.time,
shellID: data.shellID ?? shell.id,
command: data.command ?? shell.command,
status: data.status ?? shell.status,
exit: data.exit ?? shell.exit,
output: data.output,
})
}
if (type === "assistant")
return defined({
metadata: data.metadata,
time: data.time,
agent: data.agent,
model: data.model,
content: Array.isArray(data.content) ? data.content.map(assistantContent) : data.content,
snapshot: data.snapshot,
finish: data.finish,
cost: data.cost,
tokens: data.tokens,
error: data.error,
retry: data.retry,
})
if (type === "compaction") {
if (data.status === "failed")
return defined({
metadata: data.metadata,
time: data.time,
status: data.status,
reason: data.reason,
error: data.error ?? genericCompactionError,
})
return defined({
metadata: data.metadata,
time: data.time,
status: data.status,
reason: data.reason,
summary: data.summary,
recent: data.recent,
})
}
if (type === "synthetic")
return defined({ metadata: data.metadata, time: data.time, text: data.text, description: data.description })
const { sessionID: _, ...current } = data
return current
}
function assistantContent(value: unknown) {
const content = object(value)
if (content.type === "text") return defined({ type: content.type, text: content.text })
if (content.type === "reasoning")
return defined({ type: content.type, text: content.text, state: content.state, time: content.time })
if (content.type !== "tool") return content
return defined({
type: content.type,
id: content.id,
name: content.name,
executed: content.executed,
providerState: content.providerState,
providerResultState: content.providerResultState,
state: toolState(content.state),
time: content.time,
})
}
function toolState(value: unknown) {
const state = object(value)
if (state.status === "pending" || state.status === "streaming")
return defined({ status: "streaming", input: state.input })
if (state.status === "running")
return defined({ status: state.status, input: state.input, structured: state.structured, content: state.content })
if (state.status === "completed")
return defined({
status: state.status,
input: state.input,
structured: state.structured,
content: state.content,
result: state.result,
})
if (state.status === "error")
return defined({
status: state.status,
input: state.input,
structured: state.structured,
content: state.content,
error: state.error,
result: state.result,
})
return state
}
function eventData(type: string, data: Record<string, unknown>, compactionReason?: "auto" | "manual") {
if (type.startsWith("session.skill.activated."))
return {
type: "session.skill.activated.1",
data: defined({ sessionID: data.sessionID, id: data.id ?? data.name, name: data.name, text: data.text }),
}
if (type.startsWith("session.compaction.started."))
return {
type: "session.compaction.started.1",
data: defined({
sessionID: data.sessionID,
reason: data.reason,
recent: data.recent ?? "",
inputID: data.inputID,
}),
}
if (type.startsWith("session.compaction.failed."))
return {
type: "session.compaction.failed.1",
data: defined({
sessionID: data.sessionID,
reason: data.reason ?? compactionReason ?? "manual",
error: data.error ?? genericCompactionError,
inputID: data.inputID,
}),
}
const revert = object(data.revert)
return {
type: "session.revert.staged.1",
data: defined({
sessionID: data.sessionID,
revert: defined({
messageID: revert.messageID,
partID: revert.partID,
snapshot: revert.snapshot,
files: Array.isArray(revert.files)
? revert.files.map((value) => {
const file = object(value)
return defined({
file: file.file ?? file.path,
patch: file.patch,
additions: file.additions,
deletions: file.deletions,
status: file.status,
})
})
: undefined,
}),
}),
}
}
const genericCompactionError = {
type: "compaction.failed",
message: "Compaction failed before recording an error",
}
function object(value: unknown): Record<string, unknown> {
return isObject(value) ? value : {}
}
function defined(value: Record<string, unknown>) {
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined))
}
+1 -1
View File
@@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
+2 -2
View File
@@ -1,6 +1,6 @@
export * as File from "./file"
import { Revert } from "@opencode-ai/schema/revert"
import { FileDiff } from "@opencode-ai/schema/file-diff"
export const Diff = Revert.FileDiff
export const Diff = FileDiff.Info
export type Diff = typeof Diff.Type
@@ -46,7 +46,7 @@ const layer = Layer.effect(
.flatMap((item) => item.info.watcher?.ignore ?? [])
const home = path.resolve(location.directory) === path.resolve(os.homedir())
if (!home) {
if (!home && location.vcs) {
yield* watcher
.subscribe({
path: location.directory,
+1 -1
View File
@@ -606,7 +606,7 @@ const layer = Layer.effect(
file,
])).text
return {
path: file,
file,
status,
additions: binary ? 0 : Number(stats[0] ?? 0),
deletions: binary ? 0 : Number(stats[1] ?? 0),
+59 -23
View File
@@ -203,6 +203,7 @@ type AttemptTime = { created: number; expires: number }
type PendingAttempt = {
status: "pending"
completing: boolean
persisting: boolean
authorization: OAuthAuthorization
integrationID: ID
methodID: MethodID
@@ -320,27 +321,61 @@ const layer = Layer.effect(
}
const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.OAuth, unknown>) {
const now = yield* Clock.currentTimeMillis
const result = yield* SynchronizedRef.modify(attempts, (current) => {
const attempt = current.get(attemptID)
if (!attempt || attempt.status !== "pending") return [undefined, current]
const terminal: TerminalAttempt = Exit.isSuccess(exit)
? { status: "complete", time: attempt.time, removeAt: now + terminalRetention }
: { status: "failed", message: message(exit.cause), time: attempt.time, removeAt: now + terminalRetention }
return [attempt, new Map(current).set(attemptID, terminal)]
})
if (!result) return
if (Exit.isSuccess(exit)) {
const implementation = state.get().integrations.get(result.integrationID)?.implementations.get(result.methodID)
yield* credentials.create({
integrationID: result.integrationID,
label: result.label ?? implementation?.label?.(exit.value),
value: exit.value,
})
yield* events.publish(Event.ConnectionUpdated, { integrationID: result.integrationID })
yield* events.publish(Event.Updated, {})
}
yield* close(result.scope)
return yield* Effect.uninterruptible(
Effect.gen(function* () {
const now = yield* Clock.currentTimeMillis
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(attemptID)
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
const next = Exit.isSuccess(exit)
? { ...match, persisting: true }
: {
status: "failed" as const,
message: message(exit.cause),
time: match.time,
removeAt: now + terminalRetention,
}
return [match, new Map(current).set(attemptID, next)]
})
if (!attempt) return
if (Exit.isFailure(exit)) {
yield* close(attempt.scope)
return
}
yield* Effect.gen(function* () {
const implementation = state
.get()
.integrations.get(attempt.integrationID)
?.implementations.get(attempt.methodID)
const persistence = yield* Effect.sync(() => attempt.label ?? implementation?.label?.(exit.value)).pipe(
Effect.flatMap((label) =>
credentials.create({
integrationID: attempt.integrationID,
label,
value: exit.value,
}),
),
Effect.asVoid,
Effect.exit,
)
const settledAt = yield* Clock.currentTimeMillis
const terminal: TerminalAttempt = Exit.isSuccess(persistence)
? { status: "complete", time: attempt.time, removeAt: settledAt + terminalRetention }
: {
status: "failed",
message: message(persistence.cause),
time: attempt.time,
removeAt: settledAt + terminalRetention,
}
// Persisting attempts cannot be cancelled, expired, or claimed again.
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
yield* events.publish(Event.Updated, {})
}).pipe(Effect.ensuring(close(attempt.scope)))
}),
)
})
const scrub = Effect.fnUntraced(function* () {
@@ -349,7 +384,7 @@ const layer = Layer.effect(
const next = new Map(current)
const scopes: Scope.Closeable[] = []
for (const [id, attempt] of current) {
if (attempt.status === "pending" && attempt.time.expires <= now) {
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
scopes.push(attempt.scope)
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
continue
@@ -432,6 +467,7 @@ const layer = Layer.effect(
new Map(current).set(id, {
status: "pending",
completing: authorization.mode === "auto",
persisting: false,
authorization,
integrationID: input.integrationID,
methodID: input.methodID,
@@ -506,7 +542,7 @@ const layer = Layer.effect(
cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) {
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
const match = current.get(attemptID)
if (!match || match.status !== "pending") return [undefined, current]
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
const next = new Map(current)
next.delete(attemptID)
return [match, next]
+38 -71
View File
@@ -5,29 +5,22 @@ import { Catalog } from "./catalog"
import { CommandV2 } from "./command"
import { Config } from "./config"
import { LayerNode } from "./effect/layer-node"
import { makeLocationNode, Node } from "./effect/app-node"
import { httpClient } from "./effect/app-node-platform"
import { Node } from "./effect/app-node"
import { EventV2 } from "./event"
import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
import { FSUtil } from "./fs-util"
import { Generate } from "./generate"
import { Form } from "./form"
import { Global } from "./global"
import { LocationWatcher } from "./filesystem/location-watcher"
import { Image } from "./image"
import { LocationWatcher } from "./filesystem/location-watcher"
import { Integration } from "./integration"
import { Location } from "./location"
import { LocationMutation } from "./location-mutation"
import { LocationServiceMap } from "./location-service-map"
import { MCP } from "./mcp/index"
import { ModelsDev } from "./models-dev"
import { Npm } from "./npm"
import { PermissionV2 } from "./permission"
import { PluginV2 } from "./plugin"
import { PluginRuntime } from "./plugin/runtime"
import { SdkPlugins } from "./plugin/sdk"
import { PluginSupervisor } from "./plugin/supervisor"
import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty"
@@ -35,7 +28,6 @@ import { QuestionV2 } from "./question"
import { Shell } from "./shell"
import { Reference } from "./reference"
import { ReferenceGuidance } from "./reference/guidance"
import { Ripgrep } from "./ripgrep"
import { SessionRunnerLLM } from "./session/runner/llm"
import { SessionRunnerModel } from "./session/runner/model"
import { SessionCompaction } from "./session/compaction"
@@ -51,49 +43,11 @@ import { SessionInstructions } from "./session/instructions"
import { McpTool } from "./tool/mcp"
import { ReadToolFileSystem } from "./tool/read-filesystem"
import { ToolRegistry } from "./tool/registry"
import { WebSearchTool } from "./tool/websearch"
import { ToolOutputStore } from "./tool-output-store"
import { Vcs } from "./vcs"
export { LocationServiceMap } from "./location-service-map"
const pluginSupervisorNode = makeLocationNode({
service: PluginSupervisor.Service,
layer: PluginSupervisor.layer,
deps: [
PluginV2.node,
SdkPlugins.node,
AgentV2.node,
Catalog.node,
CommandV2.node,
Config.node,
EventV2.node,
FileMutation.node,
FileSystem.node,
FSUtil.node,
Global.node,
httpClient,
Image.node,
Integration.node,
Location.node,
LocationMutation.node,
ModelsDev.node,
Npm.node,
PermissionV2.node,
PluginRuntime.node,
Form.node,
ReadToolFileSystem.node,
Reference.node,
Ripgrep.node,
SessionInstructions.node,
SessionTodo.node,
Shell.node,
SkillV2.node,
ToolRegistry.toolsNode,
WebSearchTool.configNode,
],
})
const locationServiceNodes = [
Location.node,
Config.node,
@@ -104,7 +58,7 @@ const locationServiceNodes = [
Catalog.node,
AISDK.node,
PluginV2.node,
pluginSupervisorNode,
PluginSupervisor.node,
ProjectCopy.node,
ProjectCopy.refreshNode,
FileSystemSearch.node,
@@ -150,31 +104,44 @@ export type LocationError = LayerNode.Error<typeof locationServices>
export function buildLocationServiceMap(
replacements: LayerNode.Replacements = [],
): Layer.Layer<LocationServiceMap.Service> {
// Structural Equal is own-key-set sensitive, so `{ directory }` (schema-decoded
// payloads omit optional keys) and `{ directory, workspaceID: undefined }` are
// different RcMap keys. The RcMap caches by the raw key before the build
// callback runs, so canonicalize at the map boundary to the key-present shape.
const canonical = (ref: Location.Ref) => Location.Ref.make({ directory: ref.directory, workspaceID: ref.workspaceID })
return Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
(ref: Location.Ref) => {
const startedAt = performance.now()
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
// Apply replacements during hoist, not afterward: replacements can
// introduce new tagged dependencies (Location.boundNode depends on
// Project), and the hoist walk is the only pass that can still slice
// those back out.
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
Effect.map(
LayerMap.make(
(ref: Location.Ref) => {
const startedAt = performance.now()
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
// Apply replacements during hoist, not afterward: replacements can
// introduce new tagged dependencies (Location.boundNode depends on
// Project), and the hoist walk is the only pass that can still slice
// those back out.
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
return LayerNode.compile(location.node).pipe(
Layer.fresh,
Layer.tap(() =>
Effect.logInfo("location services booted", {
directory: ref.directory,
workspaceID: ref.workspaceID,
durationMs: Math.round(performance.now() - startedAt),
}),
),
Layer.provide(LayerNode.compile(location.hoisted)),
)
},
{ idleTimeToLive: "60 minutes" },
return LayerNode.compile(location.node).pipe(
Layer.fresh,
Layer.tap(() =>
Effect.logInfo("location services booted", {
directory: ref.directory,
workspaceID: ref.workspaceID,
durationMs: Math.round(performance.now() - startedAt),
}),
),
Layer.provide(LayerNode.compile(location.hoisted)),
)
},
{ idleTimeToLive: "60 minutes" },
),
(inner) => ({
...inner,
get: (ref: Location.Ref) => inner.get(canonical(ref)),
contextEffect: (ref: Location.Ref) => inner.contextEffect(canonical(ref)),
invalidate: (ref: Location.Ref) => inner.invalidate(canonical(ref)),
}),
),
)
}
+118 -16
View File
@@ -3,7 +3,7 @@ export * as MCPClient from "./client"
import path from "node:path"
import { execFile } from "node:child_process"
import { pathToFileURL } from "node:url"
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
@@ -21,6 +21,7 @@ import {
ListToolsResultSchema,
PromptListChangedNotificationSchema,
PromptSchema,
ResourceListChangedNotificationSchema,
type LoggingMessageNotification,
LoggingMessageNotificationSchema,
ToolListChangedNotificationSchema,
@@ -68,11 +69,13 @@ export interface ToolDefinition {
export interface PromptDefinition {
readonly name: string
readonly description: string | undefined
readonly arguments: ReadonlyArray<{
readonly name: string
readonly description: string | undefined
readonly required: boolean | undefined
}> | undefined
readonly arguments:
| ReadonlyArray<{
readonly name: string
readonly description: string | undefined
readonly required: boolean | undefined
}>
| undefined
}
export interface PromptMessage {
@@ -84,6 +87,28 @@ export interface PromptResult {
readonly messages: ReadonlyArray<PromptMessage>
}
export interface ResourceDefinition {
readonly name: string
readonly uri: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export interface ResourceTemplateDefinition {
readonly name: string
readonly uriTemplate: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export type ResourceContentPart =
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType: string | undefined }
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType: string | undefined }
export interface ReadResourceResult {
readonly contents: ReadonlyArray<ResourceContentPart>
}
export type CallToolContent =
| { readonly type: "text"; readonly text: string }
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
@@ -124,6 +149,12 @@ export interface Connection {
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
/** Lists the server's resources; returns [] when the server doesn't advertise resource support. */
readonly resources: () => Effect.Effect<ResourceDefinition[], Error>
/** Lists the server's resource templates; returns [] when the server doesn't advertise resource support. */
readonly resourceTemplates: () => Effect.Effect<ResourceTemplateDefinition[], Error>
/** Reads one resource; returns undefined when the server doesn't advertise resource support. */
readonly readResource: (input: { readonly uri: string }) => Effect.Effect<ReadResourceResult | undefined, Error>
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
readonly prompt: (input: {
readonly name: string
@@ -141,6 +172,8 @@ export interface Connection {
readonly onToolsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
readonly onPromptsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its resource catalog changed. */
readonly onResourcesChanged: (callback: () => void) => void
}
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
@@ -168,7 +201,8 @@ export const connect = Effect.fnUntraced(function* (
},
})
}
if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
if (!URL.canParse(config.url))
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
return new StreamableHTTPClientTransport(new URL(config.url), {
requestInit: config.headers ? { headers: config.headers } : undefined,
authProvider,
@@ -202,10 +236,7 @@ export const connect = Effect.fnUntraced(function* (
}).pipe(Effect.exit)
if (Exit.isSuccess(exit)) {
yield* Effect.addFinalizer(() =>
cleanupStdioDescendants(transport).pipe(
Effect.andThen(Effect.promise(() => client.close())),
Effect.ignore,
),
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
)
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
@@ -257,7 +288,9 @@ export const connect = Effect.fnUntraced(function* (
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
),
)
return prompts.map((prompt) => ({
name: prompt.name,
@@ -269,6 +302,74 @@ export const connect = Effect.fnUntraced(function* (
})),
}))
}),
resources: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const resources = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
(result) => result.resources,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
),
)
return resources.map((resource) => ({
name: resource.name,
uri: resource.uri,
description: resource.description,
mimeType: resource.mimeType,
}))
}),
resourceTemplates: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const templates = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, {
timeout: catalogTimeout,
}),
(result) => result.resourceTemplates,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
),
)
return templates.map((template) => ({
name: template.name,
uriTemplate: template.uriTemplate,
description: template.description,
mimeType: template.mimeType,
}))
}),
readResource: (input) =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return undefined
const result = yield* Effect.tryPromise({
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
),
)
return {
contents: result.contents.map(
(part): ResourceContentPart =>
"text" in part
? { type: "text", uri: part.uri, text: part.text, mimeType: part.mimeType }
: { type: "blob", uri: part.uri, blob: part.blob, mimeType: part.mimeType },
),
}
}),
prompt: (input) =>
Effect.tryPromise({
try: (signal) =>
@@ -328,13 +429,14 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.prompts?.listChanged) return
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
},
onResourcesChanged: (callback) => {
if (!client.getServerCapabilities()?.resources?.listChanged) return
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
},
} satisfies Connection
}
yield* cleanupStdioDescendants(transport).pipe(
Effect.andThen(Effect.promise(() => transport.close())),
Effect.ignore,
)
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
const error = Cause.squash(exit.cause)
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
+80 -50
View File
@@ -83,48 +83,16 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
messages: Schema.Array(PromptMessage),
}) {}
export class Resource extends Schema.Class<Resource>("MCP.Resource")({
server: ServerName,
name: Schema.String,
uri: Schema.String,
description: Schema.String.pipe(Schema.optional),
mimeType: Schema.String.pipe(Schema.optional),
}) {}
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
server: ServerName,
name: Schema.String,
uriTemplate: Schema.String,
description: Schema.String.pipe(Schema.optional),
mimeType: Schema.String.pipe(Schema.optional),
}) {}
export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}) {}
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
]).pipe(Schema.toTaggedUnion("type"))
export type ResourceContentPart = typeof ResourceContentPart.Type
export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
server: ServerName,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}) {}
export const Resource = Mcp.Resource
export type Resource = Mcp.Resource
export const ResourceTemplate = Mcp.ResourceTemplate
export type ResourceTemplate = Mcp.ResourceTemplate
export const ResourceCatalog = Mcp.ResourceCatalog
export type ResourceCatalog = Mcp.ResourceCatalog
export const ResourceContentPart = Mcp.ResourceContentPart
export type ResourceContentPart = Mcp.ResourceContentPart
export const ResourceContent = Mcp.ResourceContent
export type ResourceContent = Mcp.ResourceContent
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
server: ServerName,
@@ -415,6 +383,24 @@ export const layer = Layer.effect(
),
})
const toResource = (server: ServerName, def: MCPClient.ResourceDefinition) =>
Resource.make({
server,
name: def.name,
uri: def.uri,
description: def.description,
mimeType: def.mimeType,
})
const toResourceTemplate = (server: ServerName, def: MCPClient.ResourceTemplateDefinition) =>
ResourceTemplate.make({
server,
name: def.name,
uriTemplate: def.uriTemplate,
description: def.description,
mimeType: def.mimeType,
})
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
connection.tools().pipe(
Effect.map((defs) => {
@@ -443,6 +429,7 @@ export const layer = Layer.effect(
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
})
@@ -458,6 +445,10 @@ export const layer = Layer.effect(
connection.onPromptsChanged(() => {
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
})
connection.onResourcesChanged(() => {
if (entry.client !== connection) return
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
})
}
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@@ -501,6 +492,7 @@ export const layer = Layer.effect(
// after the initial registration sweep and emits no list-changed notification would otherwise
// stay invisible to the model.
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
return
@@ -557,11 +549,6 @@ export const layer = Layer.effect(
concurrency: "unbounded",
discard: true,
})
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
const target = yield* requireServer(server)
yield* Deferred.await(target.entry.startup)
})
return Service.of({
servers: Effect.fn("MCP.servers")(function* () {
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
@@ -637,11 +624,54 @@ export const layer = Layer.effect(
}),
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
yield* whenAllReady
return new ResourceCatalog({ resources: [], templates: [] })
const catalogs = yield* Effect.forEach(
Array.from(runtime),
([name, entry]) => {
if (!entry.client) return Effect.succeed({ resources: [], templates: [] })
return Effect.all(
{
resources: entry.client.resources().pipe(Effect.catch(() => Effect.succeed([]))),
templates: entry.client.resourceTemplates().pipe(Effect.catch(() => Effect.succeed([]))),
},
{ concurrency: "unbounded" },
).pipe(
Effect.map((catalog) => ({
resources: catalog.resources.map((def) => toResource(name, def)),
templates: catalog.templates.map((def) => toResourceTemplate(name, def)),
})),
)
},
{ concurrency: "unbounded" },
)
return ResourceCatalog.make({
resources: catalogs
.flatMap((catalog) => catalog.resources)
.toSorted(
(a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name) || a.uri.localeCompare(b.uri),
),
templates: catalogs
.flatMap((catalog) => catalog.templates)
.toSorted(
(a, b) =>
a.server.localeCompare(b.server) ||
a.name.localeCompare(b.name) ||
a.uriTemplate.localeCompare(b.uriTemplate),
),
})
}),
readResource: Effect.fn("MCP.readResource")(function* (input) {
yield* gate(input.server)
return undefined
const target = yield* requireServer(input.server)
yield* Deferred.await(target.entry.startup)
if (!target.entry.client) return undefined
const result = yield* target.entry.client
.readResource({ uri: input.uri })
.pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!result) return undefined
return ResourceContent.make({
server: target.name,
uri: input.uri,
contents: result.contents,
})
}),
})
}),

Some files were not shown because too many files have changed in this diff Show More