feat: normalize project paths and enhance team member handling
- Updated WorktreeGrouper to normalize project paths before resolving identities and branches, ensuring consistent behavior across platforms. - Enhanced TeamDataService to improve lead name resolution and prevent self-notification for comments on lead-owned tasks in solo teams. - Added new utility functions in TeamDataService for better lead name handling and solo team validation. - Improved TeamMemberLogsFinder to exclude owner sessions when the owner is the team lead, refining log retrieval logic. - Updated UI components to reflect changes in team member suggestions based on solo team status. Made-with: Cursor
This commit is contained in:
parent
032d9b478b
commit
1d07d6bb96
9 changed files with 177 additions and 33 deletions
|
|
@ -60,11 +60,12 @@ export class WorktreeGrouper {
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
projects.map(async (project) => {
|
projects.map(async (project) => {
|
||||||
const identity = await gitIdentityResolver.resolveIdentity(project.path);
|
const normalizedProjectPath = path.normalize(project.path);
|
||||||
|
const identity = await gitIdentityResolver.resolveIdentity(normalizedProjectPath);
|
||||||
projectIdentities.set(project.id, identity);
|
projectIdentities.set(project.id, identity);
|
||||||
|
|
||||||
// Also get branch name for display
|
// Also get branch name for display
|
||||||
const branch = await gitIdentityResolver.getBranch(project.path);
|
const branch = await gitIdentityResolver.getBranch(normalizedProjectPath);
|
||||||
projectBranches.set(project.id, branch);
|
projectBranches.set(project.id, branch);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -137,19 +138,18 @@ export class WorktreeGrouper {
|
||||||
for (const [groupId, group] of repoGroups) {
|
for (const [groupId, group] of repoGroups) {
|
||||||
const worktrees: Worktree[] = await Promise.all(
|
const worktrees: Worktree[] = await Promise.all(
|
||||||
group.projects.map(async (project) => {
|
group.projects.map(async (project) => {
|
||||||
|
const normalizedProjectPath = path.normalize(project.path);
|
||||||
const branch = group.branches.get(project.id) ?? null;
|
const branch = group.branches.get(project.id) ?? null;
|
||||||
const isMainWorktree = !(await gitIdentityResolver.isWorktree(project.path));
|
const isMainWorktree = !(await gitIdentityResolver.isWorktree(normalizedProjectPath));
|
||||||
// Use filtered sessions instead of raw sessions
|
// Use filtered sessions instead of raw sessions
|
||||||
const filteredSessions = projectFilteredSessions.get(project.id) ?? [];
|
const filteredSessions = projectFilteredSessions.get(project.id) ?? [];
|
||||||
// Detect worktree source for badge display
|
// Detect worktree source for badge display
|
||||||
// project.path may use forward slashes (e.g. decodePath() returns "C:/...").
|
// project.path may use forward slashes (e.g. decodePath() returns "C:/...").
|
||||||
// detectWorktreeSource splits on path.sep, so normalize to the current platform first.
|
// detectWorktreeSource splits on path.sep, so normalize to the current platform first.
|
||||||
const source = await gitIdentityResolver.detectWorktreeSource(
|
const source = await gitIdentityResolver.detectWorktreeSource(normalizedProjectPath);
|
||||||
path.normalize(project.path)
|
|
||||||
);
|
|
||||||
// Use source-aware display name generation
|
// Use source-aware display name generation
|
||||||
const displayName = await gitIdentityResolver.getWorktreeDisplayName(
|
const displayName = await gitIdentityResolver.getWorktreeDisplayName(
|
||||||
project.path,
|
normalizedProjectPath,
|
||||||
source,
|
source,
|
||||||
branch,
|
branch,
|
||||||
isMainWorktree
|
isMainWorktree
|
||||||
|
|
|
||||||
|
|
@ -914,11 +914,14 @@ export class TeamDataService {
|
||||||
const comment = await this.taskWriter.addComment(teamName, taskId, text);
|
const comment = await this.taskWriter.addComment(teamName, taskId, text);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [tasks, toolPath] = await Promise.all([
|
const [tasks, toolPath, config, metaMembers] = await Promise.all([
|
||||||
this.taskReader.getTasks(teamName),
|
this.taskReader.getTasks(teamName),
|
||||||
this.toolsInstaller.ensureInstalled(),
|
this.toolsInstaller.ensureInstalled(),
|
||||||
|
this.configReader.getConfig(teamName).catch(() => null),
|
||||||
|
this.membersMetaStore.getMembers(teamName).catch(() => []),
|
||||||
]);
|
]);
|
||||||
const task = tasks.find((t) => t.id === taskId);
|
const task = tasks.find((t) => t.id === taskId);
|
||||||
|
const leadName = this.resolveLeadNameFromConfig(config);
|
||||||
|
|
||||||
// Auto-clear needsClarification: "user" on UI comment
|
// Auto-clear needsClarification: "user" on UI comment
|
||||||
// UI comments always have author "user" (TeamTaskWriter default)
|
// UI comments always have author "user" (TeamTaskWriter default)
|
||||||
|
|
@ -927,6 +930,15 @@ export class TeamDataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task?.owner) {
|
if (task?.owner) {
|
||||||
|
// Solo team UX: if the user comments on a lead-owned task, don't echo the
|
||||||
|
// comment back as an inbox notification from the lead. The comment is already visible.
|
||||||
|
if (
|
||||||
|
this.isSoloTeamFromMembers(config, metaMembers, leadName) &&
|
||||||
|
this.isLeadOwner(task.owner, leadName)
|
||||||
|
) {
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
const parts = [
|
const parts = [
|
||||||
`Comment on task #${taskId} "${task.subject}":\n\n${text}`,
|
`Comment on task #${taskId} "${task.subject}":\n\n${text}`,
|
||||||
`\n${AGENT_BLOCK_OPEN}`,
|
`\n${AGENT_BLOCK_OPEN}`,
|
||||||
|
|
@ -934,7 +946,6 @@ export class TeamDataService {
|
||||||
`node "${toolPath}" --team ${teamName} task comment ${taskId} --text "<your reply>" --from "<your-name>"`,
|
`node "${toolPath}" --team ${teamName} task comment ${taskId} --text "<your reply>" --from "<your-name>"`,
|
||||||
AGENT_BLOCK_CLOSE,
|
AGENT_BLOCK_CLOSE,
|
||||||
];
|
];
|
||||||
const leadName = await this.resolveLeadName(teamName);
|
|
||||||
await this.sendMessage(teamName, {
|
await this.sendMessage(teamName, {
|
||||||
member: task.owner,
|
member: task.owner,
|
||||||
from: leadName,
|
from: leadName,
|
||||||
|
|
@ -953,17 +964,48 @@ export class TeamDataService {
|
||||||
return this.inboxWriter.sendMessage(teamName, request);
|
return this.inboxWriter.sendMessage(teamName, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveLeadNameFromConfig(config: TeamConfig | null): string {
|
||||||
|
if (!config) return 'team-lead';
|
||||||
|
const lead = config.members?.find((m) => m.role?.toLowerCase().includes('lead'));
|
||||||
|
return lead?.name ?? config.members?.[0]?.name ?? 'team-lead';
|
||||||
|
}
|
||||||
|
|
||||||
private async resolveLeadName(teamName: string): Promise<string> {
|
private async resolveLeadName(teamName: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const config = await this.configReader.getConfig(teamName);
|
const config = await this.configReader.getConfig(teamName);
|
||||||
if (!config) return 'team-lead';
|
return this.resolveLeadNameFromConfig(config);
|
||||||
const lead = config.members?.find((m) => m.role?.toLowerCase().includes('lead'));
|
|
||||||
return lead?.name ?? config.members?.[0]?.name ?? 'team-lead';
|
|
||||||
} catch {
|
} catch {
|
||||||
return 'team-lead';
|
return 'team-lead';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isLeadOwner(owner: string, leadName: string): boolean {
|
||||||
|
const normalized = owner.trim();
|
||||||
|
if (!normalized) return false;
|
||||||
|
return normalized === leadName || normalized === 'team-lead';
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSoloTeamFromMembers(
|
||||||
|
config: TeamConfig | null,
|
||||||
|
metaMembers: TeamMember[],
|
||||||
|
leadName: string
|
||||||
|
): boolean {
|
||||||
|
const configMembers = config?.members ?? [];
|
||||||
|
const combined = [...configMembers, ...(metaMembers ?? [])];
|
||||||
|
|
||||||
|
const activeNonLead = combined.filter((m) => {
|
||||||
|
const name = m.name?.trim();
|
||||||
|
if (!name) return false;
|
||||||
|
if (m.removedAt) return false;
|
||||||
|
if (m.agentType === 'team-lead') return false;
|
||||||
|
if (name === 'team-lead') return false;
|
||||||
|
if (name === leadName) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return activeNonLead.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
async sendDirectToLead(
|
async sendDirectToLead(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
leadName: string,
|
leadName: string,
|
||||||
|
|
|
||||||
|
|
@ -170,12 +170,19 @@ export class TeamMemberLogsFinder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedOwner =
|
||||||
|
typeof options?.owner === 'string' ? options.owner.trim() : options?.owner;
|
||||||
|
const isLeadOwner =
|
||||||
|
typeof normalizedOwner === 'string' &&
|
||||||
|
normalizedOwner.length > 0 &&
|
||||||
|
normalizedOwner.toLowerCase() === leadMemberName.toLowerCase();
|
||||||
const includeOwnerSessions =
|
const includeOwnerSessions =
|
||||||
options?.status === 'in_progress' &&
|
options?.status === 'in_progress' &&
|
||||||
typeof options?.owner === 'string' &&
|
typeof normalizedOwner === 'string' &&
|
||||||
options.owner.trim().length > 0;
|
normalizedOwner.length > 0 &&
|
||||||
|
!isLeadOwner;
|
||||||
if (includeOwnerSessions) {
|
if (includeOwnerSessions) {
|
||||||
const ownerLogs = await this.findMemberLogs(teamName, options.owner!.trim());
|
const ownerLogs = await this.findMemberLogs(teamName, normalizedOwner);
|
||||||
|
|
||||||
const TASK_LOG_INTERVAL_GRACE_MS = 10_000;
|
const TASK_LOG_INTERVAL_GRACE_MS = 10_000;
|
||||||
const fallbackRecentMs = 30 * 60_000; // if caller doesn't supply intervals/since, avoid pulling in old owner history
|
const fallbackRecentMs = 30 * 60_000; // if caller doesn't supply intervals/since, avoid pulling in old owner history
|
||||||
|
|
|
||||||
|
|
@ -426,20 +426,22 @@ export const CreateTeamDialog = ({
|
||||||
|
|
||||||
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
||||||
() =>
|
() =>
|
||||||
members
|
soloTeam
|
||||||
.filter((m) => m.name.trim())
|
? [{ id: 'team-lead', name: 'team-lead', subtitle: 'Team Lead', color: 'blue' }]
|
||||||
.map((m, index) => ({
|
: members
|
||||||
id: m.id,
|
.filter((m) => m.name.trim())
|
||||||
name: m.name.trim(),
|
.map((m, index) => ({
|
||||||
subtitle:
|
id: m.id,
|
||||||
m.roleSelection === CUSTOM_ROLE
|
name: m.name.trim(),
|
||||||
? m.customRole.trim() || undefined
|
subtitle:
|
||||||
: m.roleSelection && m.roleSelection !== NO_ROLE
|
m.roleSelection === CUSTOM_ROLE
|
||||||
? m.roleSelection
|
? m.customRole.trim() || undefined
|
||||||
: undefined,
|
: m.roleSelection && m.roleSelection !== NO_ROLE
|
||||||
color: getMemberColor(index),
|
? m.roleSelection
|
||||||
})),
|
: undefined,
|
||||||
[members]
|
color: getMemberColor(index),
|
||||||
|
})),
|
||||||
|
[members, soloTeam]
|
||||||
);
|
);
|
||||||
|
|
||||||
const effectiveModel = useMemo(
|
const effectiveModel = useMemo(
|
||||||
|
|
@ -711,7 +713,7 @@ export const CreateTeamDialog = ({
|
||||||
maxRows={12}
|
maxRows={12}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onValueChange={promptDraft.setValue}
|
onValueChange={promptDraft.setValue}
|
||||||
suggestions={mentionSuggestions}
|
suggestions={soloTeam ? [] : mentionSuggestions}
|
||||||
projectPath={effectiveCwd || null}
|
projectPath={effectiveCwd || null}
|
||||||
chips={promptChipDraft.chips}
|
chips={promptChipDraft.chips}
|
||||||
onChipRemove={promptChipDraft.removeChip}
|
onChipRemove={promptChipDraft.removeChip}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ interface ContinuousScrollViewProps {
|
||||||
onContentChanged: (filePath: string, content: string) => void;
|
onContentChanged: (filePath: string, content: string) => void;
|
||||||
onDiscard: (filePath: string) => void;
|
onDiscard: (filePath: string) => void;
|
||||||
onSave: (filePath: string) => void;
|
onSave: (filePath: string) => void;
|
||||||
|
onAcceptNewFile: (filePath: string) => void;
|
||||||
|
onRejectNewFile: (filePath: string) => void;
|
||||||
onRestoreMissingFile?: (filePath: string, content: string) => void;
|
onRestoreMissingFile?: (filePath: string, content: string) => void;
|
||||||
onVisibleFileChange: (filePath: string) => void;
|
onVisibleFileChange: (filePath: string) => void;
|
||||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
@ -72,6 +74,8 @@ export const ContinuousScrollView = ({
|
||||||
onContentChanged,
|
onContentChanged,
|
||||||
onDiscard,
|
onDiscard,
|
||||||
onSave,
|
onSave,
|
||||||
|
onAcceptNewFile,
|
||||||
|
onRejectNewFile,
|
||||||
onRestoreMissingFile,
|
onRestoreMissingFile,
|
||||||
onVisibleFileChange,
|
onVisibleFileChange,
|
||||||
scrollContainerRef,
|
scrollContainerRef,
|
||||||
|
|
@ -224,6 +228,8 @@ export const ContinuousScrollView = ({
|
||||||
onToggleCollapse={handleToggleCollapse}
|
onToggleCollapse={handleToggleCollapse}
|
||||||
onDiscard={onDiscard}
|
onDiscard={onDiscard}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
|
onAcceptNewFile={onAcceptNewFile}
|
||||||
|
onRejectNewFile={onRejectNewFile}
|
||||||
onRestoreMissingFile={onRestoreMissingFile}
|
onRestoreMissingFile={onRestoreMissingFile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ interface FileSectionHeaderProps {
|
||||||
onDiscard: (filePath: string) => void;
|
onDiscard: (filePath: string) => void;
|
||||||
onSave: (filePath: string) => void;
|
onSave: (filePath: string) => void;
|
||||||
onRestoreMissingFile?: (filePath: string, content: string) => void;
|
onRestoreMissingFile?: (filePath: string, content: string) => void;
|
||||||
|
onAcceptNewFile?: (filePath: string) => void;
|
||||||
|
onRejectNewFile?: (filePath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileSectionHeader = ({
|
export const FileSectionHeader = ({
|
||||||
|
|
@ -39,6 +41,8 @@ export const FileSectionHeader = ({
|
||||||
onDiscard,
|
onDiscard,
|
||||||
onSave,
|
onSave,
|
||||||
onRestoreMissingFile,
|
onRestoreMissingFile,
|
||||||
|
onAcceptNewFile,
|
||||||
|
onRejectNewFile,
|
||||||
}: FileSectionHeaderProps): React.ReactElement => {
|
}: FileSectionHeaderProps): React.ReactElement => {
|
||||||
const isMissingOnDisk = fileContent?.contentSource === 'unavailable';
|
const isMissingOnDisk = fileContent?.contentSource === 'unavailable';
|
||||||
const restoreContent =
|
const restoreContent =
|
||||||
|
|
@ -141,6 +145,38 @@ export const FileSectionHeader = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1.5" data-no-collapse>
|
<div className="ml-auto flex items-center gap-1.5" data-no-collapse>
|
||||||
|
{file.isNewFile && (onAcceptNewFile || onRejectNewFile) && (
|
||||||
|
<div className="mr-1 flex items-center gap-1.5">
|
||||||
|
{onAcceptNewFile && (
|
||||||
|
<button
|
||||||
|
onClick={() => onAcceptNewFile(file.filePath)}
|
||||||
|
disabled={applying}
|
||||||
|
className={[
|
||||||
|
'rounded px-2 py-1 text-xs font-medium transition-colors disabled:opacity-50',
|
||||||
|
fileDecision === 'accepted'
|
||||||
|
? 'bg-green-500/25 text-green-300'
|
||||||
|
: 'bg-green-500/15 text-green-400 hover:bg-green-500/25',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onRejectNewFile && (
|
||||||
|
<button
|
||||||
|
onClick={() => onRejectNewFile(file.filePath)}
|
||||||
|
disabled={applying}
|
||||||
|
className={[
|
||||||
|
'rounded px-2 py-1 text-xs font-medium transition-colors disabled:opacity-50',
|
||||||
|
fileDecision === 'rejected'
|
||||||
|
? 'bg-red-500/25 text-red-300'
|
||||||
|
: 'bg-red-500/15 text-red-400 hover:bg-red-500/25',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{canRestore && restoreContent != null && (
|
{canRestore && restoreContent != null && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,11 @@ const TreeItem = ({
|
||||||
>
|
>
|
||||||
{node.name}
|
{node.name}
|
||||||
</span>
|
</span>
|
||||||
|
{node.data.isNewFile && (
|
||||||
|
<span className="shrink-0 rounded bg-green-500/20 px-1.5 py-0.5 text-[10px] font-medium text-green-400">
|
||||||
|
new
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="ml-1 flex shrink-0 items-center gap-1">
|
<span className="ml-1 flex shrink-0 items-center gap-1">
|
||||||
{node.data.linesAdded > 0 && (
|
{node.data.linesAdded > 0 && (
|
||||||
<span className="text-green-400">+{node.data.linesAdded}</span>
|
<span className="text-green-400">+{node.data.linesAdded}</span>
|
||||||
|
|
|
||||||
|
|
@ -427,4 +427,50 @@ describe('TeamMemberLogsFinder', () => {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('findLogsForTask does not auto-include owner sessions when owner is team-lead', async () => {
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-task-lead-owner-'));
|
||||||
|
setClaudeBasePathOverride(tmpDir);
|
||||||
|
|
||||||
|
const teamName = 't6';
|
||||||
|
const projectPath = '/Users/test/proj6';
|
||||||
|
const projectId = '-Users-test-proj6';
|
||||||
|
const leadSessionId = 's6';
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'teams', teamName, 'config.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
name: teamName,
|
||||||
|
projectPath,
|
||||||
|
leadSessionId,
|
||||||
|
members: [{ name: 'team-lead', agentType: 'team-lead' }],
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||||
|
await fs.mkdir(path.join(projectRoot, leadSessionId, 'subagents'), { recursive: true });
|
||||||
|
|
||||||
|
// Lead session exists but does NOT reference taskId 42.
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: '2026-01-01T00:00:00.000Z',
|
||||||
|
type: 'assistant',
|
||||||
|
message: { role: 'assistant', content: [{ type: 'text', text: 'Hello' }] },
|
||||||
|
}) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const finder = new TeamMemberLogsFinder();
|
||||||
|
const logs = await finder.findLogsForTask(teamName, '42', {
|
||||||
|
owner: 'team-lead',
|
||||||
|
status: 'in_progress',
|
||||||
|
intervals: [{ startedAt: '2026-01-01T10:00:00.000Z' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// We only want sessions that explicitly reference the task id.
|
||||||
|
expect(logs).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -740,10 +740,10 @@ describe('teamctl.js', () => {
|
||||||
expect(String(comments[0].createdAt)).toMatch(ISO_RE);
|
expect(String(comments[0].createdAt)).toMatch(ISO_RE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults author to "agent" when --from is not specified', () => {
|
it('defaults author to inferred lead name when --from is not specified', () => {
|
||||||
run(claudeDir, ['task', 'comment', '1', '--text', 'No author']);
|
run(claudeDir, ['task', 'comment', '1', '--text', 'No author']);
|
||||||
const comments = readTask(claudeDir, '1').comments as Record<string, unknown>[];
|
const comments = readTask(claudeDir, '1').comments as Record<string, unknown>[];
|
||||||
expect(comments[0].author).toBe('agent');
|
expect(comments[0].author).toBe('alice');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends inbox notification to owner (skip self-notification)', () => {
|
it('sends inbox notification to owner (skip self-notification)', () => {
|
||||||
|
|
@ -1899,7 +1899,7 @@ describe('teamctl.js', () => {
|
||||||
// from=true → not a string → defaults to 'agent'
|
// from=true → not a string → defaults to 'agent'
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
const comments = readTask(claudeDir, '1').comments as { author: string; text: string }[];
|
const comments = readTask(claudeDir, '1').comments as { author: string; text: string }[];
|
||||||
expect(comments[0].author).toBe('agent'); // not "--text"
|
expect(comments[0].author).toBe('alice'); // not "--text"
|
||||||
expect(comments[0].text).toBe('Hello');
|
expect(comments[0].text).toBe('Hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue