fix: resolve 25 pre-publish blockers

- postinstall.mjs: fix alias package detection
- migrate-legacy-plugin-entry: dedupe + regression tests
- task_system: default consistency across runtime paths
- task() contract: consistent tool behavior
- runtime model selection, tool cap, stale-task cancellation
- recovery sanitization, context-limit gating
- Ralph semantic DONE hardening, Atlas fallback persistence
- native-skill description/content, skill path traversal guard
- publish workflow: platform awaited via reusable workflow job
- release: version edits reapplied before commit/tag
- JSONC plugin migration: top-level plugin key safety
- cold-cache: user fallback models skip disconnected providers
- docs/version/release framing updates

Verified: bun test (4599 pass), tsc --noEmit clean, bun run build clean
This commit is contained in:
YeonGyu-Kim
2026-03-28 15:24:18 +09:00
parent 44b039bef6
commit d2c576c510
62 changed files with 1264 additions and 292 deletions
@@ -0,0 +1,105 @@
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
const replaceEmptyTextPartsAsync = mock(() => Promise.resolve(false))
const injectTextPartAsync = mock(() => Promise.resolve(false))
const findMessagesWithEmptyTextPartsFromSDK = mock(() => Promise.resolve([] as string[]))
mock.module("../../shared", () => ({
normalizeSDKResponse: (response: { data?: unknown[] }) => response.data ?? [],
}))
mock.module("../../shared/logger", () => ({
log: () => {},
}))
mock.module("../../shared/opencode-storage-detection", () => ({
isSqliteBackend: () => true,
}))
mock.module("../session-recovery/storage", () => ({
findEmptyMessages: () => [],
findMessagesWithEmptyTextParts: () => [],
injectTextPart: () => false,
replaceEmptyTextParts: () => false,
}))
mock.module("../session-recovery/storage/empty-text", () => ({
replaceEmptyTextPartsAsync,
findMessagesWithEmptyTextPartsFromSDK,
}))
mock.module("../session-recovery/storage/text-part-injector", () => ({
injectTextPartAsync,
}))
async function importFreshMessageBuilder(): Promise<typeof import("./message-builder")> {
return import(`./message-builder?test=${Date.now()}-${Math.random()}`)
}
afterAll(() => {
mock.restore()
})
describe("sanitizeEmptyMessagesBeforeSummarize", () => {
beforeEach(() => {
replaceEmptyTextPartsAsync.mockReset()
replaceEmptyTextPartsAsync.mockResolvedValue(false)
injectTextPartAsync.mockReset()
injectTextPartAsync.mockResolvedValue(false)
findMessagesWithEmptyTextPartsFromSDK.mockReset()
findMessagesWithEmptyTextPartsFromSDK.mockResolvedValue([])
})
test("#given sqlite message with tool content and empty text part #when sanitizing #then it fixes the mixed-content message", async () => {
const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await importFreshMessageBuilder()
const client = {
session: {
messages: mock(() => Promise.resolve({
data: [
{
info: { id: "msg-1" },
parts: [
{ type: "tool_result", text: "done" },
{ type: "text", text: "" },
],
},
],
})),
},
} as never
findMessagesWithEmptyTextPartsFromSDK.mockResolvedValue(["msg-1"])
replaceEmptyTextPartsAsync.mockResolvedValue(true)
const fixedCount = await sanitizeEmptyMessagesBeforeSummarize("ses-1", client)
expect(fixedCount).toBe(1)
expect(replaceEmptyTextPartsAsync).toHaveBeenCalledWith(client, "ses-1", "msg-1", PLACEHOLDER_TEXT)
expect(injectTextPartAsync).not.toHaveBeenCalled()
})
test("#given sqlite message with mixed content and failed replacement #when sanitizing #then it injects the placeholder text part", async () => {
const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await importFreshMessageBuilder()
const client = {
session: {
messages: mock(() => Promise.resolve({
data: [
{
info: { id: "msg-2" },
parts: [
{ type: "tool_use", text: "call" },
{ type: "text", text: "" },
],
},
],
})),
},
} as never
findMessagesWithEmptyTextPartsFromSDK.mockResolvedValue(["msg-2"])
injectTextPartAsync.mockResolvedValue(true)
const fixedCount = await sanitizeEmptyMessagesBeforeSummarize("ses-2", client)
expect(fixedCount).toBe(1)
expect(injectTextPartAsync).toHaveBeenCalledWith(client, "ses-2", "msg-2", PLACEHOLDER_TEXT)
})
})