@coze-arch/cli 0.0.17 → 0.0.19-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  9. package/lib/__templates__/nuxt-vue/package.json +9 -2
  10. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  11. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  12. package/lib/__templates__/pi-agent/.coze +10 -0
  13. package/lib/__templates__/pi-agent/AGENTS.md +144 -0
  14. package/lib/__templates__/pi-agent/README.md +216 -0
  15. package/lib/__templates__/pi-agent/_gitignore +3 -0
  16. package/lib/__templates__/pi-agent/_npmrc +23 -0
  17. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  18. package/lib/__templates__/pi-agent/docs/project-overview.md +374 -0
  19. package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
  20. package/lib/__templates__/pi-agent/package.json +63 -0
  21. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  22. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  23. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
  24. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
  29. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
  30. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
  31. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  32. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  33. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  34. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  35. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  36. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  37. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  38. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  39. package/lib/__templates__/pi-agent/src/config.ts +708 -0
  40. package/lib/__templates__/pi-agent/src/core.ts +218 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
  68. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
  69. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  70. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
  71. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  72. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  73. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  74. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  75. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  76. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  77. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  78. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  79. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  80. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  81. package/lib/__templates__/pi-agent/template.config.js +45 -0
  82. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  83. package/lib/__templates__/pi-agent/tests/config.test.ts +315 -0
  84. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
  85. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  86. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  87. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  88. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  89. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  90. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  91. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  92. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  93. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  94. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  95. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  96. package/lib/__templates__/taro/.coze +1 -0
  97. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  98. package/lib/__templates__/taro/package.json +1 -1
  99. package/lib/__templates__/templates.json +18 -31
  100. package/lib/__templates__/vite/.coze +1 -0
  101. package/lib/__templates__/vite/package.json +3 -1
  102. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  103. package/lib/cli.js +13 -2
  104. package/package.json +1 -1
@@ -0,0 +1,171 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { ValidationError, readModelsResponse, saveModelsRequest } from "../src/dashboard/api/models.js";
7
+
8
+ function createConfigFile(config: unknown): { configPath: string; cleanup: () => void } {
9
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-bot-dashboard-models-"));
10
+ const workspaceDir = join(tempDir, "workspace");
11
+ const configPath = join(workspaceDir, "config.json");
12
+
13
+ mkdirSync(workspaceDir, { recursive: true });
14
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
15
+
16
+ return {
17
+ configPath,
18
+ cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
19
+ };
20
+ }
21
+
22
+ test("readModelsResponse exposes default model and selectable options", () => {
23
+ const { configPath, cleanup } = createConfigFile({
24
+ agents: {
25
+ defaults: {
26
+ model: {
27
+ primary: "coze/auto",
28
+ },
29
+ },
30
+ },
31
+ models: {
32
+ providers: {
33
+ coze: {
34
+ api: "openai-completions",
35
+ apiKey: "${COZE_API_KEY}",
36
+ baseUrl: "https://coze.example.com/v1",
37
+ authHeader: true,
38
+ models: [
39
+ {
40
+ id: "auto",
41
+ name: "Auto",
42
+ input: ["text", "image"],
43
+ reasoning: true,
44
+ contextWindow: 200000,
45
+ maxTokens: 8192,
46
+ },
47
+ ],
48
+ },
49
+ },
50
+ },
51
+ });
52
+
53
+ const result = readModelsResponse({ configPath });
54
+ cleanup();
55
+
56
+ assert.equal(result.defaultModel, "coze/auto");
57
+ assert.deepEqual(result.options, [{ value: "coze/auto", label: "coze / Auto" }]);
58
+ });
59
+
60
+ test("saveModelsRequest updates default model while preserving unrelated config", () => {
61
+ const { configPath, cleanup } = createConfigFile({
62
+ agents: {
63
+ defaults: {
64
+ model: {
65
+ primary: "coze/auto",
66
+ },
67
+ },
68
+ },
69
+ models: {
70
+ providers: {
71
+ coze: {
72
+ api: "openai-completions",
73
+ apiKey: "${COZE_API_KEY}",
74
+ baseUrl: "https://coze.example.com/v1",
75
+ headers: {
76
+ "X-Provider": "keep-me",
77
+ },
78
+ models: [
79
+ {
80
+ id: "auto",
81
+ name: "Auto",
82
+ input: ["text"],
83
+ reasoning: false,
84
+ contextWindow: 200000,
85
+ maxTokens: 8192,
86
+ cost: {
87
+ input: 0,
88
+ output: 0,
89
+ cacheRead: 0,
90
+ cacheWrite: 0,
91
+ },
92
+ },
93
+ {
94
+ id: "glm-4.7",
95
+ name: "GLM-4.7",
96
+ input: ["text"],
97
+ reasoning: false,
98
+ contextWindow: 256000,
99
+ maxTokens: 8192,
100
+ cost: {
101
+ input: 0,
102
+ output: 0,
103
+ cacheRead: 0,
104
+ cacheWrite: 0,
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ },
111
+ channels: {
112
+ feishu: {
113
+ enabled: true,
114
+ },
115
+ },
116
+ });
117
+
118
+ saveModelsRequest({
119
+ configPath,
120
+ body: {
121
+ models: {
122
+ defaultModel: "coze/glm-4.7",
123
+ },
124
+ },
125
+ });
126
+
127
+ const saved = JSON.parse(readFileSync(configPath, "utf-8")) as {
128
+ agents: { defaults: { model: { primary: string } } };
129
+ models: {
130
+ providers: {
131
+ coze: {
132
+ apiKey: string;
133
+ baseUrl: string;
134
+ authHeader: boolean;
135
+ headers: Record<string, string>;
136
+ models: Array<Record<string, unknown>>;
137
+ };
138
+ };
139
+ };
140
+ channels: { feishu: { enabled: boolean } };
141
+ };
142
+
143
+ cleanup();
144
+
145
+ assert.equal(saved.agents.defaults.model.primary, "coze/glm-4.7");
146
+ assert.deepEqual(saved.models.providers.coze.headers, {
147
+ "X-Provider": "keep-me",
148
+ });
149
+ assert.equal(saved.channels.feishu.enabled, true);
150
+ });
151
+
152
+ test("saveModelsRequest rejects invalid defaultModel", () => {
153
+ const { configPath, cleanup } = createConfigFile({
154
+ agents: { defaults: { model: { primary: "coze/auto" } } },
155
+ models: { providers: { coze: { models: [{ id: "auto", name: "Auto" }] } } },
156
+ });
157
+ try {
158
+ assert.throws(() => {
159
+ saveModelsRequest({
160
+ configPath,
161
+ body: {
162
+ models: {
163
+ defaultModel: "coze/does-not-exist",
164
+ },
165
+ },
166
+ });
167
+ }, ValidationError);
168
+ } finally {
169
+ cleanup();
170
+ }
171
+ });
@@ -0,0 +1,149 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createFeishuChannel } from "../src/channels/feishu/index.js";
4
+
5
+ test("feishu channel adds and removes thinking reaction around replies", async () => {
6
+ const operations: string[] = [];
7
+ let nextReactionId = 0;
8
+
9
+ const channel = createFeishuChannel(
10
+ {
11
+ thinkingReaction: {
12
+ enabled: true,
13
+ emojiType: "OneSecond"
14
+ },
15
+ transport: {
16
+ addReaction({ messageId, emojiType }) {
17
+ nextReactionId += 1;
18
+ operations.push(`add:${messageId}:${emojiType}`);
19
+ return {
20
+ reactionId: `reaction-${nextReactionId}`,
21
+ emojiType
22
+ };
23
+ },
24
+ removeReaction({ messageId, reactionId, emojiType }) {
25
+ operations.push(`remove:${messageId}:${reactionId}:${emojiType}`);
26
+ },
27
+ send(message) {
28
+ operations.push(`send:${message.replyToMessageId}:${message.text}`);
29
+ }
30
+ }
31
+ },
32
+ {
33
+ async onMessage() {
34
+ operations.push("handler:start");
35
+ await Promise.resolve();
36
+ operations.push("handler:end");
37
+ return {
38
+ text: "reply text"
39
+ };
40
+ }
41
+ }
42
+ );
43
+
44
+ const result = await channel.simulateIncomingText({
45
+ text: "hello",
46
+ senderId: "user-1",
47
+ conversationId: "dm-1",
48
+ isDirectMessage: true
49
+ });
50
+
51
+ assert.equal(result.handled, true);
52
+ assert.equal(result.reply?.text, "reply text");
53
+ assert.deepEqual(operations, [
54
+ `add:${result.message?.messageId}:OneSecond`,
55
+ "handler:start",
56
+ "handler:end",
57
+ `remove:${result.message?.messageId}:reaction-1:OneSecond`,
58
+ `send:${result.message?.messageId}:reply text`
59
+ ]);
60
+ });
61
+
62
+ test("feishu channel defaults thinking reaction emoji to OneSecond", async () => {
63
+ const operations: string[] = [];
64
+
65
+ const channel = createFeishuChannel(
66
+ {
67
+ transport: {
68
+ addReaction({ messageId, emojiType }) {
69
+ operations.push(`add:${messageId}:${emojiType}`);
70
+ return {
71
+ reactionId: "reaction-1",
72
+ emojiType
73
+ };
74
+ },
75
+ removeReaction({ messageId, reactionId, emojiType }) {
76
+ operations.push(`remove:${messageId}:${reactionId}:${emojiType}`);
77
+ },
78
+ send(message) {
79
+ operations.push(`send:${message.replyToMessageId}:${message.text}`);
80
+ }
81
+ }
82
+ },
83
+ {
84
+ onMessage() {
85
+ return {
86
+ text: "reply text"
87
+ };
88
+ }
89
+ }
90
+ );
91
+
92
+ const result = await channel.simulateIncomingText({
93
+ text: "hello",
94
+ senderId: "user-default",
95
+ conversationId: "dm-default",
96
+ isDirectMessage: true
97
+ });
98
+
99
+ assert.equal(result.handled, true);
100
+ assert.deepEqual(operations, [
101
+ `add:${result.message?.messageId}:OneSecond`,
102
+ `remove:${result.message?.messageId}:reaction-1:OneSecond`,
103
+ `send:${result.message?.messageId}:reply text`
104
+ ]);
105
+ });
106
+
107
+ test("feishu channel still replies when thinking reaction fails", async () => {
108
+ const operations: string[] = [];
109
+
110
+ const channel = createFeishuChannel(
111
+ {
112
+ thinkingReaction: {
113
+ enabled: true,
114
+ emojiType: "OneSecond"
115
+ },
116
+ transport: {
117
+ addReaction() {
118
+ operations.push("add-failed");
119
+ throw new Error("reaction unavailable");
120
+ },
121
+ send(message) {
122
+ operations.push(`send:${message.replyToMessageId}:${message.text}`);
123
+ }
124
+ }
125
+ },
126
+ {
127
+ onMessage() {
128
+ operations.push("handler");
129
+ return {
130
+ text: "reply after failure"
131
+ };
132
+ }
133
+ }
134
+ );
135
+
136
+ const result = await channel.simulateIncomingText({
137
+ text: "hello",
138
+ senderId: "user-2",
139
+ conversationId: "dm-2",
140
+ isDirectMessage: true
141
+ });
142
+
143
+ assert.equal(result.handled, true);
144
+ assert.deepEqual(operations, [
145
+ "add-failed",
146
+ "handler",
147
+ `send:${result.message?.messageId}:reply after failure`
148
+ ]);
149
+ });
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mergeStreamingText } from "../src/channels/feishu/streaming-card.js";
4
+
5
+ test("mergeStreamingText prefers progressive full-text updates", () => {
6
+ assert.equal(mergeStreamingText("", "hel"), "hel");
7
+ assert.equal(mergeStreamingText("hel", "hello"), "hello");
8
+ assert.equal(mergeStreamingText("hello", "hello world"), "hello world");
9
+ });
10
+
11
+ test("mergeStreamingText merges overlapping chunks without duplicating suffixes", () => {
12
+ assert.equal(mergeStreamingText("hello", "lo world"), "hello world");
13
+ assert.equal(mergeStreamingText("abc", "bcdef"), "abcdef");
14
+ });
15
+
@@ -0,0 +1,73 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { SettingsManager } from "@mariozechner/pi-coding-agent";
7
+ import {
8
+ buildPiResourceLoaderOptions,
9
+ createPiResourceLoader,
10
+ getPiResourcesDir
11
+ } from "../src/pi-resources.js";
12
+
13
+ test("buildPiResourceLoaderOptions includes repo-managed resource directories", () => {
14
+ const cwd = mkdtempSync(path.join(tmpdir(), "pi-bot-pi-resources-"));
15
+ const options = buildPiResourceLoaderOptions({
16
+ cwd,
17
+ agentDir: cwd,
18
+ settingsManager: SettingsManager.inMemory()
19
+ });
20
+
21
+ const resourcesDir = getPiResourcesDir();
22
+ assert.ok((options.additionalExtensionPaths ?? []).includes(path.join(resourcesDir, "extensions", "test-ping.ts")));
23
+ assert.ok(
24
+ (options.additionalExtensionPaths ?? []).includes(
25
+ path.join(resourcesDir, "extensions", "preference-memory", "index.ts")
26
+ )
27
+ );
28
+ assert.deepEqual(options.additionalSkillPaths, [path.join(resourcesDir, "skills")]);
29
+ assert.deepEqual(options.additionalPromptTemplatePaths, [path.join(resourcesDir, "prompts")]);
30
+ assert.deepEqual(options.agentsFilesOverride?.({ agentsFiles: [{ path: "/tmp/AGENTS.md", content: "x" }] }), {
31
+ agentsFiles: []
32
+ });
33
+ });
34
+
35
+ test("buildPiResourceLoaderOptions prefers local .pi SYSTEM.md over repo-managed SYSTEM.md", () => {
36
+ const cwd = mkdtempSync(path.join(tmpdir(), "pi-bot-pi-resources-local-system-"));
37
+ mkdirSync(path.join(cwd, ".pi"), { recursive: true });
38
+ writeFileSync(path.join(cwd, ".pi", "SYSTEM.md"), "local-system");
39
+
40
+ const options = buildPiResourceLoaderOptions({
41
+ cwd,
42
+ agentDir: cwd,
43
+ settingsManager: SettingsManager.inMemory()
44
+ });
45
+
46
+ assert.equal(options.systemPrompt, path.join(cwd, ".pi", "SYSTEM.md"));
47
+ });
48
+
49
+ test("buildPiResourceLoaderOptions falls back to repo-managed SYSTEM.md when no local override exists", () => {
50
+ const cwd = mkdtempSync(path.join(tmpdir(), "pi-bot-pi-resources-source-system-"));
51
+ const options = buildPiResourceLoaderOptions({
52
+ cwd,
53
+ agentDir: cwd,
54
+ settingsManager: SettingsManager.inMemory()
55
+ });
56
+
57
+ assert.equal(options.systemPrompt, path.join(getPiResourcesDir(), "SYSTEM.md"));
58
+ });
59
+
60
+ test("createPiResourceLoader loads repo-managed extensions without errors", async () => {
61
+ const cwd = mkdtempSync(path.join(tmpdir(), "pi-bot-pi-resources-loader-"));
62
+ const loader = createPiResourceLoader({
63
+ cwd,
64
+ agentDir: cwd,
65
+ settingsManager: SettingsManager.inMemory()
66
+ });
67
+
68
+ await loader.reload();
69
+
70
+ const result = loader.getExtensions();
71
+ assert.equal(result.errors.length, 0);
72
+ assert.ok(result.extensions.length >= 2);
73
+ });
@@ -0,0 +1,43 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ extractPreferencesFromText,
5
+ getPreferencesFilePath,
6
+ renderPreferencesDocument,
7
+ upsertPreferencesDocument
8
+ } from "../pi-resources/extensions/preference-memory/index.js";
9
+
10
+ test("getPreferencesFilePath points to workspace root preference.md", () => {
11
+ const filePath = getPreferencesFilePath("/tmp/pi-workspace");
12
+ assert.equal(filePath, "/tmp/pi-workspace/preference.md");
13
+ });
14
+
15
+ test("extractPreferencesFromText captures stable reply and project preferences", () => {
16
+ const preferences = extractPreferencesFromText(
17
+ "以后都用中文回复,回答尽量简洁。这个项目统一使用 pnpm。后续资源都写到 workspace/.pi。"
18
+ );
19
+
20
+ assert.deepEqual(
21
+ preferences.map((preference) => preference.key).sort(),
22
+ ["package-manager", "reply-language", "reply-style", "runtime-resource-dir"]
23
+ );
24
+ });
25
+
26
+ test("extractPreferencesFromText ignores temporary one-off instructions", () => {
27
+ const preferences = extractPreferencesFromText("这次请用中文回复,先不要删文件。");
28
+ assert.deepEqual(preferences, []);
29
+ });
30
+
31
+ test("upsertPreferencesDocument replaces existing values instead of appending duplicates", () => {
32
+ const initial = renderPreferencesDocument([
33
+ { key: "reply-language", section: "回复偏好", label: "回复语言", value: "中文" }
34
+ ]);
35
+ const updated = upsertPreferencesDocument(initial, [
36
+ { key: "reply-language", section: "回复偏好", label: "回复语言", value: "英文" },
37
+ { key: "package-manager", section: "项目约定", label: "包管理器", value: "pnpm" }
38
+ ]);
39
+
40
+ assert.match(updated, /回复语言:英文/);
41
+ assert.doesNotMatch(updated, /回复语言:中文/);
42
+ assert.match(updated, /包管理器:pnpm/);
43
+ });
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { SessionStore } from "../src/session-store.js";
7
+
8
+ test("SessionStore persists index + transcript header and can reload", () => {
9
+ const root = mkdtempSync(join(tmpdir(), "pi-bot-session-store-"));
10
+ try {
11
+ const store1 = new SessionStore(root);
12
+ store1.load();
13
+
14
+ const key = "dashboard:dm:dashboard-user";
15
+ const record1 = store1.ensureSession(key);
16
+
17
+ assert.equal(record1.sessionKey, key);
18
+ assert.ok(record1.sessionId);
19
+ assert.ok(record1.sessionFile.endsWith(".jsonl"));
20
+ assert.ok(existsSync(record1.sessionFile));
21
+
22
+ const firstLine = readFileSync(record1.sessionFile, "utf-8").split(/\r?\n/)[0] ?? "";
23
+ const header = JSON.parse(firstLine) as { type?: string; id?: string };
24
+ assert.equal(header.type, "session");
25
+ assert.equal(header.id, record1.sessionId);
26
+
27
+ const store2 = new SessionStore(root);
28
+ store2.load();
29
+ assert.deepEqual(store2.listSessionKeys(), [key]);
30
+ const record2 = store2.ensureSession(key);
31
+ assert.equal(record2.sessionId, record1.sessionId);
32
+ assert.equal(record2.sessionFile, record1.sessionFile);
33
+ } finally {
34
+ rmSync(root, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test("SessionStore reset archives prior transcript and rotates sessionId", () => {
39
+ const root = mkdtempSync(join(tmpdir(), "pi-bot-session-store-reset-"));
40
+ try {
41
+ const store = new SessionStore(root);
42
+ store.load();
43
+
44
+ const key = "feishu:dm:user-1";
45
+ const first = store.ensureSession(key);
46
+ assert.ok(existsSync(first.sessionFile));
47
+
48
+ const second = store.resetSession(key);
49
+ assert.notEqual(second.sessionId, first.sessionId);
50
+ assert.notEqual(second.sessionFile, first.sessionFile);
51
+ assert.ok(existsSync(second.sessionFile));
52
+
53
+ const sessionsDir = join(root, ".pi-bot", "sessions");
54
+ const archiveDir = join(sessionsDir, "archive");
55
+ // Old file should have been moved into archive (best-effort rename).
56
+ assert.ok(existsSync(archiveDir));
57
+ } finally {
58
+ rmSync(root, { recursive: true, force: true });
59
+ }
60
+ });
61
+